Retrieved Context Chunks
The RAG debugging surface: ranked passages with similarity as a meter, matched spans in real mark elements, and filter chips wired as a radiogroup so arrows move between them.
The published footnotes under an answer — ordered list, real cite and time elements, the claim each source supports, and a staleness warning written in words rather than shown as an amber dot.
components/source-citation-list.tsxnpx hoverlab add source-citation-list
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Start a page with this section — add more, order them, and leave with the page source.
Database table: Q3 revenue cohortswarehouse.arr_monthly
Supports: The 6.1% churn figure for accounts under 20 seats
2,463 accounts · lost ARR $1.24M · seat_band < 20
Read
Database table: Activation funnel, Jul–Sepwarehouse.events
Supports: The claim that a second integration predicts retention
Accounts with ≥2 connected sources churned at 1.6%; with 1 source, 6.4%.
Read
Document: Pricing handbookdocs/pricing-handbook.md
Supports: That the volume break applies per order rather than per quarter
The break is applied per order, not per quarter, so a single large order beats two smaller ones of the same total volume.
Read
This document has changed twice since it was indexed.
Web page: Supplier lead times (opens in a new tab)supplier.example.com
Supports: The 21-day reorder window
Read
Every figure above traces to one of these. Nothing was inferred without a source.
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
/**
* <SourceCitationList> — the footnote apparatus under a generated answer:
* numbered sources, what each one contributed, and when it was read.
*
* Distinct from `<ContextChunkCards>` on purpose. Chunks are the *debugging*
* view — everything retrieved, scored, including what was ignored. This is
* the *published* view: only what the answer actually leaned on, in the
* order it was cited, written for a reader who wants to check a claim.
*
* What it gets right:
*
* - It is an `<ol>`. Citations are numbered and the numbers are referenced
* from the prose, so the list must genuinely be ordered — a `<ul>` with
* hand-drawn numerals breaks the moment one is removed.
* - Each entry is a `<cite>` inside a link, which is the element for the
* title of a referenced work, and the `<time>` carries a real `dateTime`
* so "2 hours ago" is machine-readable rather than a frozen string.
* - A staleness warning is text, not a colour. "Read 4 months ago" on a
* live page is the single most useful thing this list can tell someone,
* and an amber dot does not say it.
* - External links announce that they leave the page — `sr-only` text, not
* a `title`, and not an icon alone.
* - Confidence in a source is expressed as the *claim it supports*, not a
* percentage. "Supports: the 6.1% figure" is checkable; "0.82" is not.
*/
import * as React from 'react'
import { AlertTriangle, Database, ExternalLink, FileText, Globe, Quote } from 'lucide-react'
export type CitationKind = 'doc' | 'table' | 'web'
export interface Citation {
id: number
title: string
origin: string
kind: CitationKind
/** The claim in the answer this source backs. */
supports: string
/** Verbatim excerpt, shown as a pull quote. */
excerpt?: string
/** ISO timestamp of when it was read. */
readAt?: string
readLabel?: string
/** Set when the source may have changed since it was read. */
stale?: string
href?: string
}
export interface SourceCitationListProps {
heading?: string
citations?: Citation[]
footnote?: string
className?: string
}
const KIND_ICON = {
doc: FileText,
table: Database,
web: Globe,
} as const
const KIND_LABEL = {
doc: 'Document',
table: 'Database table',
web: 'Web page',
} as const
const DEFAULT_CITATIONS: Citation[] = [
{
id: 1,
title: 'Q3 revenue cohorts',
origin: 'warehouse.arr_monthly',
kind: 'table',
supports: 'The 6.1% churn figure for accounts under 20 seats',
excerpt: '2,463 accounts · lost ARR $1.24M · seat_band < 20',
readAt: '2026-08-11T09:14:00Z',
readLabel: '2 hours ago',
},
{
id: 2,
title: 'Activation funnel, Jul–Sep',
origin: 'warehouse.events',
kind: 'table',
supports: 'The claim that a second integration predicts retention',
excerpt: 'Accounts with ≥2 connected sources churned at 1.6%; with 1 source, 6.4%.',
readAt: '2026-08-11T09:15:00Z',
readLabel: '2 hours ago',
},
{
id: 3,
title: 'Pricing handbook',
origin: 'docs/pricing-handbook.md',
kind: 'doc',
supports: 'That the volume break applies per order rather than per quarter',
excerpt:
'The break is applied per order, not per quarter, so a single large order beats two smaller ones of the same total volume.',
readAt: '2026-04-02T11:00:00Z',
readLabel: '4 months ago',
stale: 'This document has changed twice since it was indexed.',
},
{
id: 4,
title: 'Supplier lead times',
origin: 'supplier.example.com',
kind: 'web',
supports: 'The 21-day reorder window',
readAt: '2026-08-11T09:16:00Z',
readLabel: '2 hours ago',
href: '#',
},
]
/*
Per-instance id, hashed from props that differ between instances.
A literal-rooted id is a latent duplicate the moment this block renders
twice on one document. `aria-labelledby` resolves to whichever element
with that id comes first, so the second copy is announced with the
first copy's label. Server component, so no `useId` -- hashing props
gives each instance its own namespace and stays stable across server and
client renders in a way a counter would not.
*/
function instanceId(...parts: (string | undefined)[]): string {
const text = parts.filter(Boolean).join('|')
let hash = 0
for (let i = 0; i < text.length; i++) hash = (Math.imul(hash, 31) + text.charCodeAt(i)) | 0
return (hash >>> 0).toString(36).slice(0, 6)
}
export function SourceCitationList({
heading = 'Sources',
citations = DEFAULT_CITATIONS,
footnote = 'Every figure above traces to one of these. Nothing was inferred without a source.',
className = '',
}: SourceCitationListProps) {
const uid = instanceId(heading, footnote)
return (
<section className={`mx-auto w-full max-w-2xl p-6 ${className}`}>
<h3 className="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground">
<Quote aria-hidden className="h-3.5 w-3.5" />
{heading}
<span className="font-mono normal-case tracking-normal">({citations.length})</span>
</h3>
<ol className="space-y-2">
{citations.map((citation) => {
const Icon = KIND_ICON[citation.kind]
const external = Boolean(citation.href)
return (
<li
key={citation.id}
id={`${uid}-source-${citation.id}`}
className="rounded-2xl border border-border/60 bg-card p-4"
>
<div className="flex gap-3">
<span
aria-hidden
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg bg-muted font-mono text-[11px] font-bold"
>
{citation.id}
</span>
<div className="min-w-0 flex-1">
<p className="flex flex-wrap items-center gap-x-2 gap-y-1">
<Icon aria-hidden className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
{external ? (
<a
href={citation.href}
className="rounded text-sm font-semibold underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="sr-only">{KIND_LABEL[citation.kind]}: </span>
<cite className="not-italic">{citation.title}</cite>
<ExternalLink aria-hidden className="ms-1 inline h-3 w-3 align-baseline" />
<span className="sr-only"> (opens in a new tab)</span>
</a>
) : (
<span className="text-sm font-semibold">
<span className="sr-only">{KIND_LABEL[citation.kind]}: </span>
<cite className="not-italic">{citation.title}</cite>
</span>
)}
<span className="truncate font-mono text-[11px] text-muted-foreground">
{citation.origin}
</span>
</p>
<p className="mt-1.5 text-xs leading-relaxed">
<span className="font-medium text-muted-foreground">Supports: </span>
{citation.supports}
</p>
{citation.excerpt ? (
<blockquote className="mt-2 border-s-2 border-border ps-3 text-xs italic leading-relaxed text-muted-foreground">
{citation.excerpt}
</blockquote>
) : null}
<p className="mt-2 text-[11px] text-muted-foreground">
Read{' '}
{citation.readAt ? (
<time dateTime={citation.readAt}>{citation.readLabel ?? citation.readAt}</time>
) : (
'at answer time'
)}
</p>
{/* Said in words. A source that has moved on since it was
read is the most consequential thing on this card, and
an amber border does not communicate it. */}
{citation.stale ? (
<p className="mt-2 flex gap-1.5 rounded-lg bg-amber-500/10 px-2.5 py-1.5 text-[11px] leading-relaxed text-amber-700 dark:text-amber-400">
<AlertTriangle aria-hidden className="mt-px h-3 w-3 shrink-0" />
{citation.stale}
</p>
) : null}
</div>
</div>
</li>
)
})}
</ol>
{footnote ? <p className="mt-3 text-xs text-muted-foreground">{footnote}</p> : null}
</section>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/source-citation-list.tsx and import it where you need the section:
import { SourceCitationList } from '@/components/source-citation-list'1 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.
Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
heading | string | 'Sources' |
citations | Citation[] | DEFAULT_CITATIONS |
footnote | string | — |
className | string | '' |
The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.
<!--
Answer Source Citations — markup from the Hoverlab catalog.
This is the block rendered once to HTML and wrapped as a component
file. It is not a port of the React source: the Tailwind classes carry
the design, which is the part that took the work, and they are the same
in every framework.
This block has no interactive behaviour, so nothing is missing.
-->
<section class="mx-auto w-full max-w-2xl p-6 ">
<h3 class="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-quote h-3.5 w-3.5" aria-hidden="true"><path d="M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"></path><path d="M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"></path></svg>Sources<span class="font-mono normal-case tracking-normal">(4)</span></h3>
<ol class="space-y-2">
<li id="s5npw8-source-1" class="rounded-2xl border border-border/60 bg-card p-4">
<div class="flex gap-3">
<span aria-hidden="true" class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg bg-muted font-mono text-[11px] font-bold">1</span>
<div class="min-w-0 flex-1">
<p class="flex flex-wrap items-center gap-x-2 gap-y-1">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-database h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true">
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
<path d="M3 5V19A9 3 0 0 0 21 19V5"></path>
<path d="M3 12A9 3 0 0 0 21 12"></path>
</svg>
<span class="text-sm font-semibold">
<span class="sr-only">Database table:</span>
<cite class="not-italic">Q3 revenue cohorts</cite>
</span>
<span class="truncate font-mono text-[11px] text-muted-foreground">warehouse.arr_monthly</span>
</p>
<p class="mt-1.5 text-xs leading-relaxed"><span class="font-medium text-muted-foreground">Supports: </span>The 6.1% churn figure for accounts under 20 seats</p>
<blockquote class="mt-2 border-s-2 border-border ps-3 text-xs italic leading-relaxed text-muted-foreground">2,463 accounts · lost ARR $1.24M · seat_band < 20</blockquote>
<p class="mt-2 text-[11px] text-muted-foreground">Read <time dateTime="2026-08-11T09:14:00Z">2 hours ago</time></p>
</div>
</div>
</li>
<li id="s5npw8-source-2" class="rounded-2xl border border-border/60 bg-card p-4">
<div class="flex gap-3">
<span aria-hidden="true" class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg bg-muted font-mono text-[11px] font-bold">2</span>
<div class="min-w-0 flex-1">
<p class="flex flex-wrap items-center gap-x-2 gap-y-1">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-database h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true">
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
<path d="M3 5V19A9 3 0 0 0 21 19V5"></path>
<path d="M3 12A9 3 0 0 0 21 12"></path>
</svg>
<span class="text-sm font-semibold">
<span class="sr-only">Database table:</span>
<cite class="not-italic">Activation funnel, Jul–Sep</cite>
</span>
<span class="truncate font-mono text-[11px] text-muted-foreground">warehouse.events</span>
</p>
<p class="mt-1.5 text-xs leading-relaxed"><span class="font-medium text-muted-foreground">Supports: </span>The claim that a second integration predicts retention</p>
<blockquote class="mt-2 border-s-2 border-border ps-3 text-xs italic leading-relaxed text-muted-foreground">Accounts with ≥2 connected sources churned at 1.6%; with 1 source, 6.4%.</blockquote>
<p class="mt-2 text-[11px] text-muted-foreground">Read <time dateTime="2026-08-11T09:15:00Z">2 hours ago</time></p>
</div>
</div>
</li>
<li id="s5npw8-source-3" class="rounded-2xl border border-border/60 bg-card p-4">
<div class="flex gap-3">
<span aria-hidden="true" class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg bg-muted font-mono text-[11px] font-bold">3</span>
<div class="min-w-0 flex-1">
<p class="flex flex-wrap items-center gap-x-2 gap-y-1">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-text h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true">
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"></path>
<path d="M14 2v4a2 2 0 0 0 2 2h4"></path>
<path d="M10 9H8"></path>
<path d="M16 13H8"></path>
<path d="M16 17H8"></path>
</svg>
<span class="text-sm font-semibold">
<span class="sr-only">Document:</span>
<cite class="not-italic">Pricing handbook</cite>
</span>
<span class="truncate font-mono text-[11px] text-muted-foreground">docs/pricing-handbook.md</span>
</p>
<p class="mt-1.5 text-xs leading-relaxed"><span class="font-medium text-muted-foreground">Supports: </span>That the volume break applies per order rather than per quarter</p>
<blockquote class="mt-2 border-s-2 border-border ps-3 text-xs italic leading-relaxed text-muted-foreground">The break is applied per order, not per quarter, so a single large order beats two smaller ones of the same total volume.</blockquote>
<p class="mt-2 text-[11px] text-muted-foreground">Read <time dateTime="2026-04-02T11:00:00Z">4 months ago</time></p>
<p class="mt-2 flex gap-1.5 rounded-lg bg-amber-500/10 px-2.5 py-1.5 text-[11px] leading-relaxed text-amber-700 dark:text-amber-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert mt-px h-3 w-3 shrink-0" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>This document has changed twice since it was indexed.</p>
</div>
</div>
</li>
<li id="s5npw8-source-4" class="rounded-2xl border border-border/60 bg-card p-4">
<div class="flex gap-3">
<span aria-hidden="true" class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg bg-muted font-mono text-[11px] font-bold">4</span>
<div class="min-w-0 flex-1">
<p class="flex flex-wrap items-center gap-x-2 gap-y-1">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path>
<path d="M2 12h20"></path>
</svg>
<a href="#" class="rounded text-sm font-semibold underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
<span class="sr-only">Web page:</span>
<cite class="not-italic">Supplier lead times</cite>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link ms-1 inline h-3 w-3 align-baseline" aria-hidden="true">
<path d="M15 3h6v6"></path>
<path d="M10 14 21 3"></path>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
</svg>
<span class="sr-only">(opens in a new tab)</span>
</a>
<span class="truncate font-mono text-[11px] text-muted-foreground">supplier.example.com</span>
</p>
<p class="mt-1.5 text-xs leading-relaxed"><span class="font-medium text-muted-foreground">Supports: </span>The 21-day reorder window</p>
<p class="mt-2 text-[11px] text-muted-foreground">Read <time dateTime="2026-08-11T09:16:00Z">2 hours ago</time></p>
</div>
</div>
</li>
</ol>
<p class="mt-3 text-xs text-muted-foreground">Every figure above traces to one of these. Nothing was inferred without a source.</p>
</section>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
Want the whole screen instead of this one section? Open a page and copy it entire.
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse Retrieval & ContextCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
The RAG debugging surface: ranked passages with similarity as a meter, matched spans in real mark elements, and filter chips wired as a radiogroup so arrows move between them.
Choosing what the assistant may read, with indexing state told honestly — a half-indexed source says so rather than showing a green tick — and the footer stating the effect of the selection, not its cardinality.
The refusal screen: says plainly that nothing was found, shows where it looked and the near-misses below threshold, and offers the two real ways out instead of inventing an answer.
What is in the context and what fell out of it — a stacked budget with the reply reserve drawn as a segment, and the dropped chunks named with the reason, which is the part nobody ships.
A green tick means the connector works, not that the answers are current. Freshness as an age in words per source, the headline taken from the stalest one, and a failed sync that still says what age it is answering from.
Retrieved sources ranked by how stale they are, because a confident answer from a two-year-old document is the failure people do not catch.
Which sources this answer may draw on, shown as a scope the user can read before asking rather than a citation list after.
How an answer is tied to its sources, and what the system does when it cannot find any.