UI kit
InlineCitation
DisplayReact · Next.jsRenders 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
| Prop | Type | Default | Description |
|---|---|---|---|
textrequired | string | — | Answer text containing [1], [2] … markers. |
sourcesrequired | Source[] | — | Sources indexed by marker number (sources[0] = [1]). |
className | string | — | Extra 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>
);
}