Citation Sources
FreeNumbered 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.
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 demoMore examples
Cards & panel
<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
| Name | Type | Default | Description |
|---|---|---|---|
| loading | boolean | false | True while the answer is being fetched. |
| error | SvDataError | string | null | null | Load failure. Use `toSvDataError()` from `@styloviz/core` to normalise an `HttpErrorResponse`. |
| variant | CitationVariant | 'inline' | Presentation: inline chips, source cards, or a sources panel. |
| answer | string | '' | The answer text the citations point into. |
| citations | readonly CitationSpan[] | [] | The citations, each carrying the span of the ANSWER it supports and the passage of the source it rests on. |
| sources | readonly SourceRef[] | [] | The sources those citations resolve to. |
| claims | readonly 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)`. |
| showCoverage | boolean | true | Show 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. |
| allowedUrlProtocols | readonly string[] | SV_SOURCE_URL_PROTOCOLS | URL 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. |
| uncitedMinChars | number | SV_UNCITED_MIN_CHARS | Shortest 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
| Event | Payload type | Description |
|---|---|---|
| retry | void | The host should re-run the failed load. |
| sourceSelect | SourceRef | The reader activated a citation chip or a source card. |
| claimsOpened | readonly 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.
<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.
// 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.