UI kit

SourceCitation

DisplayReact · Next.js

A presentational component that maps the sources array from a Ragiment pipeline 1:1 — document name, page, and relevance score, each expandable to the full chunk text. Compose it under any answer.

<SourceCitation />

Live preview

4 sources

Retrieval-augmented generation grounds an LLM's answer in retrieved documents, reducing hallucination by constraining the model to provided context.
Hybrid retrieval combines dense vector similarity with BM25 lexical matching, so both semantic paraphrases and exact keywords/identifiers are caught.
A cross-encoder reranker re-scores the top candidates before they reach the LLM, lifting precision and trimming the context window.
Chunk size and overlap materially affect answer quality; tune them to your document structure rather than using a global default.

Installation

No dependencies to install — just React + Tailwind CSS. It's a presentational component (no data fetching), so the same source works unchanged in React and Next.js (App or Pages Router) — there's no separate variant. Copy it into your project (e.g. components/SourceCitation.tsx) and import it.

Usage

usage.tsx
import { SourceCitation } from "@/components/SourceCitation";

export default function Example() {
  return (
    <SourceCitation sources={result.sources} />
  );
}

Props

PropTypeDefaultDescription
sourcesrequiredSource[]The sources array from /query (content, source, page, score).
expandablebooleantrueAllow each source to expand to its full text.
classNamestringExtra classes on the container.

Component source copy-paste ready

Presentational component — no data fetching, so the same source works unchanged in React and Next.js. There's no separate Next.js variant to copy.

SourceCitation.tsx
"use client";

import { useState } from "react";

export interface Source {
  /** The retrieved chunk text. */
  content: string;
  /** Document name / path the chunk came from. */
  source?: string;
  /** Page number, when available. */
  page?: number;
  /** Relevance score in the 0–1 range. */
  score?: number;
}

interface SourceCitationProps {
  sources: Source[];
  /** Allow each source to expand to its full text. Default: true. */
  expandable?: boolean;
  className?: string;
}

/**
 * SourceCitation — renders the source chunks behind a RAG answer with the
 * document name, page, and relevance score. Drop it under any answer; it maps
 * the `sources` array returned by a Ragiment pipeline 1:1.
 */
export function SourceCitation({ sources, expandable = true, className = "" }: SourceCitationProps) {
  const [open, setOpen] = useState<number | null>(null);
  if (!sources?.length) return null;

  return (
    <div className={`flex flex-col gap-2 ${className}`}>
      <p className="text-xs font-medium uppercase tracking-wide text-zinc-500">
        {sources.length} source{sources.length > 1 ? "s" : ""}
      </p>
      {sources.map((s, i) => {
        const isOpen = open === i;
        const preview = s.content.length > 160 ? s.content.slice(0, 160) + "…" : s.content;
        return (
          <div
            key={i}
            className="rounded-lg border border-zinc-200 bg-white text-sm dark:border-zinc-800 dark:bg-zinc-900"
          >
            <button
              type="button"
              onClick={() => expandable && setOpen(isOpen ? null : i)}
              className="flex w-full items-center justify-between gap-3 px-3 py-2 text-left"
            >
              <span className="flex min-w-0 items-center gap-2">
                <span className="flex h-5 w-5 flex-none items-center justify-center rounded bg-zinc-100 text-[11px] font-semibold text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">
                  {i + 1}
                </span>
                <span className="truncate font-medium text-zinc-700 dark:text-zinc-200">
                  {s.source || "Untitled source"}
                  {s.page != null && <span className="text-zinc-400"> · p.{s.page}</span>}
                </span>
              </span>
              {typeof s.score === "number" && (
                <span className="flex-none rounded bg-emerald-50 px-1.5 py-0.5 text-[11px] font-medium text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400">
                  {(s.score * 100).toFixed(0)}%
                </span>
              )}
            </button>
            <div className="px-3 pb-2 text-[13px] leading-relaxed text-zinc-500 dark:text-zinc-400">
              {expandable && isOpen ? s.content : preview}
            </div>
          </div>
        );
      })}
    </div>
  );
}