UI kit

RetrievalInspector

DevReact · Next.js

A collapsible developer panel showing exactly which chunks were retrieved (rank, source, page, score, text) and the retrieval latency. Drop it under an answer during development to debug retrieval quality.

<RetrievalInspector />

Live preview

query: how does hybrid retrieval work?

#1 · rag-overview.md:10.940

Retrieval-augmented generation grounds an LLM's answer in retrieved documents, reducing hallucination by constraining the model to provided context.

#2 · retrieval.md:30.880

Hybrid retrieval combines dense vector similarity with BM25 lexical matching, so both semantic paraphrases and exact keywords/identifiers are caught.

#3 · reranking.md:20.810

A cross-encoder reranker re-scores the top candidates before they reach the LLM, lifting precision and trimming the context window.

#4 · chunking.md:50.770

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

Usage

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

export default function Example() {
  return (
    <RetrievalInspector sources={result.sources} query={q} latencyMs={result.latency_ms} />
  );
}

Props

PropTypeDefaultDescription
sourcesrequiredSource[]The retrieved chunks from /query.
querystringThe query that produced these results.
latencyMsnumberRetrieval latency to display.
defaultOpenbooleantrueStart expanded.

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.

RetrievalInspector.tsx
"use client";

import { useState } from "react";

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

interface RetrievalInspectorProps {
  sources: Source[];
  query?: string;
  latencyMs?: number;
  /** Start expanded. Default: true. */
  defaultOpen?: boolean;
  className?: string;
}

/**
 * RetrievalInspector — a developer panel that shows exactly which chunks were
 * retrieved (rank, source, score, text) before generation. Drop it under an
 * answer during development to debug retrieval quality.
 */
export function RetrievalInspector({ sources, query, latencyMs, defaultOpen = true, className = "" }: RetrievalInspectorProps) {
  const [open, setOpen] = useState(defaultOpen);

  return (
    <div className={`overflow-hidden rounded-xl border border-zinc-200 bg-white font-mono text-xs dark:border-zinc-800 dark:bg-zinc-950 ${className}`}>
      <button
        type="button"
        onClick={() => setOpen(!open)}
        className="flex w-full items-center justify-between gap-2 border-b border-zinc-200 px-3 py-2 dark:border-zinc-800"
      >
        <span className="flex items-center gap-2 font-semibold text-zinc-600 dark:text-zinc-300">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ transform: open ? "rotate(90deg)" : "none" }}><path d="m9 18 6-6-6-6" /></svg>
          Retrieved context
          <span className="rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] text-zinc-500 dark:bg-zinc-800">{sources.length}</span>
        </span>
        {typeof latencyMs === "number" && <span className="text-zinc-400">{latencyMs} ms</span>}
      </button>

      {open && (
        <div className="max-h-72 overflow-y-auto">
          {query && <p className="border-b border-zinc-100 px-3 py-1.5 text-zinc-400 dark:border-zinc-900">query: {query}</p>}
          {sources.map((s, i) => (
            <div key={i} className="border-b border-zinc-100 px-3 py-2 last:border-0 dark:border-zinc-900">
              <div className="mb-1 flex items-center justify-between text-[11px]">
                <span className="text-zinc-500">#{i + 1} · {s.source || "unknown"}{s.page != null ? `:${s.page}` : ""}</span>
                {typeof s.score === "number" && <span className="text-emerald-500">{s.score.toFixed(3)}</span>}
              </div>
              <p className="leading-relaxed text-zinc-600 dark:text-zinc-400">
                {s.content.slice(0, 240)}{s.content.length > 240 ? "…" : ""}
              </p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}