UI kit

InlineCitation

DisplayReact · Next.js

Renders answer text and converts inline [n] markers into interactive superscripts that reveal the cited source chunk on hover or click. Pairs with the citation format your generation prompt already emits.

<InlineCitation />

Live preview

Hybrid retrieval combines dense and sparse search , and a cross-encoder reranks results before generation . Hover a marker to see its source.

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/InlineCitation.tsx) and import it.

Usage

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

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

Props

PropTypeDefaultDescription
textrequiredstringAnswer text containing [1], [2] … markers.
sourcesrequiredSource[]Sources indexed by marker number (sources[0] = [1]).
classNamestringExtra classes on the wrapper.

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.

InlineCitation.tsx
"use client";

import { useState } from "react";

export interface Source {
  content: string;
  source?: string;
  page?: number;
  score?: number;
}

interface InlineCitationProps {
  /** Answer text containing [1], [2] … markers. */
  text: string;
  /** Sources indexed by marker number (sources[0] backs the [1] marker). */
  sources: Source[];
  className?: string;
}

/**
 * InlineCitation — renders answer text and turns [n] markers into interactive
 * superscripts that reveal the cited source chunk on hover/click. Pairs with
 * the citation style your generation prompt already produces.
 */
export function InlineCitation({ text, sources, className = "" }: InlineCitationProps) {
  const parts = text.split(/(\[\d+\])/g);
  return (
    <span className={`leading-relaxed ${className}`}>
      {parts.map((part, i) => {
        const m = /^\[(\d+)\]$/.exec(part);
        if (m) {
          const n = parseInt(m[1], 10);
          const src = sources[n - 1];
          if (src) return <Marker key={i} n={n} source={src} />;
        }
        return <span key={i}>{part}</span>;
      })}
    </span>
  );
}

function Marker({ n, source }: { n: number; source: Source }) {
  const [open, setOpen] = useState(false);
  return (
    <span
      className="relative inline-block"
      onMouseEnter={() => setOpen(true)}
      onMouseLeave={() => setOpen(false)}
    >
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className="mx-0.5 inline-flex h-4 min-w-[1rem] items-center justify-center rounded bg-zinc-200 px-1 align-super text-[10px] font-semibold text-zinc-600 transition hover:bg-zinc-300 dark:bg-zinc-700 dark:text-zinc-200"
      >
        {n}
      </button>
      {open && (
        <span className="absolute bottom-full left-1/2 z-10 mb-1 w-64 -translate-x-1/2 rounded-lg border border-zinc-200 bg-white p-2.5 text-left text-xs shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
          <span className="mb-1 block font-medium text-zinc-700 dark:text-zinc-200">
            {source.source || "Source"}{source.page != null ? ` · p.${source.page}` : ""}
          </span>
          <span className="block leading-relaxed text-zinc-500 dark:text-zinc-400">
            {source.content.slice(0, 180)}{source.content.length > 180 ? "…" : ""}
          </span>
        </span>
      )}
    </span>
  );
}