Chat Sources
Shows where a reply came from. Hovering or clicking a citation previews the cited passage and its source. Checked citations recheck access each time they open, and a count opens the list of every source the reply used.
Usage
import { ChatSources, InlineCitation, type ChatSource } from "@tulipfarm/sprout/chat-sources";
const sources: ChatSource[] = [
{
id: "checklist",
title: "Supplier onboarding checklist",
location: "Note · Before the first order",
excerpt: "Collect the W-9 and bank details before the first purchase order.",
href: "/notes/checklist",
},
];
<p>
Collect the tax form first. <InlineCitation source={sources[0]} number={1} />
</p>
<ChatSources sources={sources} />ChatSource:{ id, title, location, excerpt, href }.hrefopens in a new tab.InlineCitation:sourceand itsnumber. It is a small secondary icon button.ChatSources:sourcesin citation order, anddefaultOpento start with the list open.CheckedCitation: the linkedlabel, astate,onOpen(called on every open), andonRetry. It is a link button that wraps with the text.
Inline citations and sources
Numbers in the reply match the numbered list. Each citation and each list item shows the title, where it lives, the passage, and an “Open source” link.
Collect the tax form and bank details before the first order. Payment terms default to Net 30, but suppliers with repeated late deliveries move to Net 15.
One source
The count reads “1 source”. The list starts closed.
Water bulbs after planting.
Empty sources
With no sources, it says none were used.
No sources were used for this response.
Checked citations
Use CheckedCitation when a source can change or access can end. Your app checks access in onOpen and passes the result as state. A removed source shows why, never its old passage. A failed check offers “Load source again”.
type CitationState =
| { kind: "idle" } | { kind: "loading" }
| { kind: "ready"; title: string; location: string; excerpt: string }
| { kind: "unavailable"; message: string }
| { kind: "error"; message: string };
<MessageResponse
renderLink={(href, label) =>
isCitation(href)
? <CheckedCitation label={text(label)} state={state} onOpen={check} onRetry={check} />
: <a href={href}>{label}</a>}
>
{reply}
</MessageResponse>Plant the bulbs in late autumn, then water them once ().
Long content
Long file names, paths, and links wrap inside a narrow chat.
Twelve deliveries still need a supplier.
Accessibility
Inline citations are named “Source 1: title”; checked citations “Source: label”. Both open a popover after a 300ms hover, on click or tap, or with Enter or Space. The pointer can move into the popover to reach “Open source”. Escape closes it and returns focus. The sources count is a disclosure button witharia-expanded, and the list is ordered. “Open source” tells screen readers it opens a new tab.
Checking access is announced as a status; unavailable and error messages as alerts. After “Load source again”, focus stays in the popover. Colors change over 120ms and the list grows over 180ms; with reduced motion both change at once.
Theme tokens
Cards use --sprout-line and --sprout-surface, secondary text --sprout-muted, and links--sprout-primary.