Next.js frontend for documind-api — upload documents, ask questions, and read answers whose citations you can click, whose claims have been checked, and whose cost is on screen.
Backend is live at https://documind-api-theta.vercel.app — set NEXT_PUBLIC_API_URL to it.
Live demo: (add your Vercel URL once the frontend is deployed)
Three things are visible here that a typical RAG demo hides, and each is deliberate:
The retrieval panel. Collapsed by default, but present: which passages, at which rank, found by vector search, keyword search, or both, and with what score. "Which retriever found this?" is the first question a reviewer asks about a hybrid-search claim, and a UI that cannot answer it is asking to be taken on faith.
Per-claim verification. Every factual sentence is checked against the passage it cites, and the
verdict is shown — supported, partial, unsupported, or unchecked. A model will cite a real
passage for a claim that passage never makes; without this panel that is invisible. Flagged claims
show by default and the clean ones collapse, so the panel is quiet when there is nothing wrong.
The cost and confidence strip. Tokens in and out, dollars, time to first token, total latency, and a confidence breakdown — not a single percentage. "87% confident" is unactionable; "retrieval strong, two claims uncited" tells the reader which part to distrust.
The config picker on the Chat page lets you ask the same question through benchmark configs A–D and watch the retrieval panel, latency, and cost change. That is the benchmark, made tangible.
| Route | What it does |
|---|---|
/ |
Upload PDFs, drive ingestion, list the indexed corpus |
/chat |
Streamed answers with citations, verification, and badges |
/evals |
The benchmark table and the HNSW recall/latency curve, read live from the API |
cp .env.example .env.local # point NEXT_PUBLIC_API_URL at your API
npm install
npm run devThe API must be running and its CORS_ORIGINS must include http://localhost:3000.
npm run build # production build
npm run typecheck # tsc --noEmit
npm run lintvercel --prodSet NEXT_PUBLIC_API_URL to the deployed API's URL. It is inlined at build time, so changing
it requires a redeploy, not just an environment-variable edit. Add the resulting frontend origin to
the API's CORS_ORIGINS — these are two separate Vercel projects and every call between them is
cross-origin.
The SSE client is hand-rolled (lib/api.ts) rather than using the Vercel AI SDK's useChat.
This stream carries citations, per-claim verification verdicts, a confidence breakdown, and a cost
report alongside the tokens. None of that maps onto the SDK's data protocol without encoding it as
opaque blobs — at which point the SDK is only parsing data: lines, which is the twenty lines the
parser already is. It also degrades correctly if the deployment buffers the response: a single
flush containing every event parses exactly the same way, and only the progressive reveal is lost.
Ingestion is client-driven. The API has no background workers on serverless, so each
POST /documents/{id}/ingest advances the pipeline as far as its time budget allows. runIngestion
in lib/api.ts is the client half of that contract. Files upload sequentially rather than in
parallel — concurrent ingestion triples the chance of hitting an embedding rate limit and stalls
every progress bar at once.
Token appends use the functional setState form. Deltas arrive faster than React commits, so
reading the previous answer out of a closure drops everything between renders.
Colours are validated, not chosen. The recall/latency chart uses categorical slots 1 and 2 from a design-system palette, with separate steps for light and dark surfaces; both pairs clear colourblind-separation, normal-vision, and contrast gates against their surface. The chart also ships a table view, so identity is never carried by colour alone.
Types are hand-written (lib/types.ts) rather than generated from the OpenAPI schema. The
surface is small, and a codegen step that must run against a live backend is a worse trade than a
file that breaks loudly when the API changes.
- Citation click-through uses the browser's PDF viewer via a
#page=Nfragment. It opens the right page; it does not highlight the exact span. Doing that properly needspdf.jswith a text layer and character offsets carried through chunking — worth doing, not done. - No conversation history. Each question is independent; there is no follow-up resolution ("what about the second one?"). The API supports a rewritten query but not a multi-turn context.
- No auth. The demo is open. The API supports an optional
X-API-Key, which the frontend does not send. - The config picker duplicates the config list from the API rather than fetching
/evals/configs, so the picker renders instantly on a cold API. Adding a config means editing both places.