Citation Sources

Free

Numbered citations attached to an answer, with the coverage strip that names the passages nothing supports. 3 variants: inline chips, source cards and a side panel.

</>sv-citation-sources airagcitationsgrounding
import { SvCitationSourcesComponent } from '@styloviz/citation-sources';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Cards & panel

example.htmlAngular
<sv-citation-sources variant="cards" [answer]="answer()" [citations]="citations()" [sources]="sources()" />
<sv-citation-sources variant="panel" [answer]="answer()" [citations]="citations()" [sources]="sources()" />

When to use

  • A RAG answer where the reader has to be able to check a claim against its source.
  • Any surface where the important question is not "what did it say" but "what is this resting on".
  • For the retrieval pipeline itself — chunks, hit scores, index health — use the RAG Knowledge Base.

Async states

  • loading
  • error
  • retry

Inputs

NameTypeDefaultDescription
loadingbooleanfalseTrue while the answer is being fetched.
errorSvDataError | string | nullnullLoad failure. Use `toSvDataError()` from `@styloviz/core` to normalise an `HttpErrorResponse`.
variantCitationVariant'inline'Presentation: inline chips, source cards, or a sources panel.
answerstring''The answer text the citations point into.
citationsreadonly CitationSpan[][]The citations, each carrying the span of the ANSWER it supports and the passage of the source it rests on.
sourcesreadonly SourceRef[][]The sources those citations resolve to.
claimsreadonly AnswerClaim[][]The answer's claims, as the host's pipeline structured them — an id and the claim's words as rendered. SUPPLY THESE AND A CITATION'S `claimId` IS ITS ANCHOR, ahead of `anchorText` and the offsets. A claim id that is not in this list is reported in the placement notice as a claim the answer no longer contains — never quietly re-placed by arithmetic. Leave it empty and `claimId` is identity only, handed back through `(claimsOpened)`.
showCoveragebooleantrueShow the coverage strip. On by default, because the figure it carries — how much of the answer is attributed, and which passages are not — is the one thing a citation UI cannot show by drawing chips.
allowedUrlProtocolsreadonly string[]SV_SOURCE_URL_PROTOCOLSURL schemes a source link may use. A SOURCE URL IS MODEL OUTPUT, WHICH IS UNTRUSTED INPUT. A retriever returns what the index holds and a model returns what it generated, and either can hand this component a `javascript:` URL. Angular renders that as a dead `unsafe:` link, which is safe and also silent — the reader clicks, nothing happens, and nobody learns the citation was fabricated. A refused URL is shown as text instead, with the scheme named: that the answer cited something unreachable is evidence about the answer.
uncitedMinCharsnumberSV_UNCITED_MIN_CHARSShortest uncited stretch worth reporting, in characters. FORTY IS A SENTENCE IN ENGLISH PROSE — and two or three sentences in Chinese, where a claim fits in a dozen characters, and most of a bullet in a summary-style answer. A connective is not an unsupported claim, so a floor has to exist; where it sits is a fact about the language and the answer shape, which this component cannot see.

Outputs

EventPayload typeDescription
retryvoidThe host should re-run the failed load.
sourceSelectSourceRefThe reader activated a citation chip or a source card.
claimsOpenedreadonly string[]The reader opened a source's passages; carries the distinct `claimId`s of the citations now on screen. Emitted only on open, and only when at least one of them carries an id. THIS IS HOW A HOST PERSISTS "THE READER CHECKED THIS". A claim id survives re-generation where offsets and wording do not, so the record still points at the right claim after the answer is rewritten.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Bound to the names this component actually exposes.

usage.htmlAngular
<sv-citation-sources
  [citations]="data.items()"
  [loading]="data.loading()"
  [error]="data.error()"
  (retry)="data.reload()"
/>

Host class

Drive the three async inputs from your own fetch.

host.component.tsTypeScript
// This component reports its state but does not emit a query, so drive
// it from your own fetch and bind the three async inputs.
readonly items = signal<Row[]>([]);
readonly loading = signal(false);
readonly error = signal<SvDataError | null>(null);

async load(): Promise<void> {
  this.loading.set(true);
  this.error.set(null);
  try {
    this.items.set(await this.api.fetchRows());
  } catch (e) {
    this.error.set(toSvDataError(e));
  } finally {
    this.loading.set(false);
  }
}
  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `citations`, `loading` and `error`.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • `[claims]` makes claim ids the **first** anchor: a citation carrying `claimId` resolves to that claim’s text before `anchorText` and before offsets, and an id that is not in the answer leaves the citation unplaced and says so rather than falling back to stale offsets. `(claimsOpened)` emits the claim ids behind a source when the reader opens it, which is what lets a host persist ‘the reader checked this’ across re-generations.
  • `answer`, `citations` and `sources` are inputs and all default to empty; import `DEMO_ANSWER` / `DEMO_CITATIONS` / `DEMO_SOURCES` for a demo.
  • Three layouts via `variant`: `inline` (superscript chips in the prose), `cards` (chips plus a source list) and `panel` (a docked source list).
  • A chip is a button, not a superscript — it emits `sourceSelect` with the source and the citation the reader activated.
  • Coverage is exposed as a strip: percentage attributed, distinct source count, and the uncited passages themselves, longest first.
  • `[uncitedMinChars]` (40, from the still-exported `SV_UNCITED_MIN_CHARS`) is the floor below which a gap is not worth reporting — the whitespace between two adjacent citations is not an unsourced claim. Forty characters is a sentence in English prose, two or three in Chinese and most of a bullet in a summary-style answer, so the figure is yours to set; `svCitationCoverage` takes it as an optional last argument.
  • Free tier: uses only free icons.
  • `anchorText` on a citation is the exact words of the ANSWER it covers, and it is placed by *finding* those words. Character offsets are the wrong anchor and every pipeline proves it: a trim, a collapsed double space, a markdown pass or a prepended greeting shifts every offset by the same amount, the chips slide off the claims they belong to, and nothing fails. Offsets stay as the fallback for a host that has not adopted it.
  • When a citation cannot be located, or lands somewhere its own offsets disagree with, the component **says so in the surface** (`[data-sv-placement-notice]`) instead of clipping it away. An answer whose citations moved is worse than an uncited one, because it looks checked.
  • `claimId` is identity rather than placement: the same claim re-worded keeps its id, so a host can persist 'the reader checked this' across re-generations.
  • `[allowedUrlProtocols]` defaults to `http:`, `https:` and `mailto:`. A source URL is model output, which is untrusted input, and Angular renders a `javascript:` href as a dead `unsafe:` link — safe, and silent. A refused URL is shown as **text** with its scheme named: that the answer cited something unreachable is evidence about the answer. Relative and protocol-relative URLs are refused rather than resolved, since this component does not know the host's base URL.

Best practices

  • Do: Pass character offsets from your generator, not a post-hoc string match. A citation matched by searching for the quote lands on the wrong sentence whenever the answer repeats a phrase.
  • Do: Read the coverage strip. A six-chip answer with a 40 % coverage score has a long unattributed passage in it, and that passage is the one to check.
  • Don't: Do not number citations per occurrence. One document quoted five times is one source; five numbers makes it read as five sources agreeing.
  • Tip: Overlapping citations are ordinary, because chunks overlap. The answer is segmented on citation boundaries so every character renders exactly once.

Accessibility

  • Chips are real buttons with accessible names naming the source they resolve to, not bare superscripts.
  • The coverage percentage is stated in words as well as drawn as a bar.
  • Uncited passages are listed as text, so the finding does not depend on noticing an absence of highlighting.

Feedback on Citation Sources

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.