ComponentsAI & chat

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.

Next access checkChecks are simulated locally; no request is sent.

Before ordering from a new supplier, collect their tax form and bank details — see the .

Payment terms default to Net 30, but suppliers with repeated late deliveries move to Net 15 ().

Usage

Numbered citations and a sources listTSX
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 }.href opens in a new tab.
  • InlineCitation: source and itsnumber. It is a small secondary icon button.
  • ChatSources: sources in citation order, anddefaultOpen to start with the list open.
  • CheckedCitation: the linked label, 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.

Inline citations and sourcesThree sources, open

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.

One sourceCloud’s playground sample, closed

Water bulbs after planting.

Empty sources

With no sources, it says none were used.

Empty sourcesThe reply used no sources

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”.

TSX
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>

Checked citation statesIdle, loading, ready, unavailable, and error

Plant the bulbs in late autumn, then water them once ().

Long content

Long file names, paths, and links wrap inside a narrow chat.

Long contentLong names, paths, and links wrap in 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.