Elements · Knowledge
Document reference
A document the answer leans on, with the quoted passage and the page to jump to.
Installation
npx shadcn@latest add "@assistant-ui/elements-document-reference"First time? Set up a runtime
Runtime components read their state from an assistant-ui runtime. Add one to an existing project:
npx assistant-ui@latest initThen wrap your app in a runtime provider:
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";
export default function App() {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({ api: "/api/chat" }),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
{/* your components */}
</AssistantRuntimeProvider>
);
}The installation guide covers new projects, templates, and API routes.
npx shadcn@latest add "@assistant-ui/elements-document-reference"Props-driven: no runtime or provider required.
DocumentReference is a citation card for one source document: a header naming the file, then a list of page anchors you can step between. With a runtime the anchors come from a tool call that read the document; standalone you already hold them.
Getting started
assistant-ui's built-in source message part covers a single document citation (a title, a filename), but not a list of page anchors with quoted passages. The closest real building block for that shape is a tool call: ask the model to read a document and return its anchors, then render the result.
Render the tool call
"use client";
import { useState } from "react";
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import {
DocumentReference,
type DocumentAnchor,
} from "@/components/assistant-ui/elements/document-reference";
type ReadDocumentResult = {
title: string;
pages: number;
anchors: DocumentAnchor[];
};
export const ReadDocumentToolUI: ToolCallMessagePartComponent<
{ query: string },
ReadDocumentResult
> = ({ result }) => {
const [activePage, setActivePage] = useState<number>();
if (!result) return null;
const page = activePage ?? result.anchors[0]?.page ?? 1;
return (
<DocumentReference
title={result.title}
pages={result.pages}
anchors={result.anchors}
activePage={page}
onJump={setActivePage}
/>
);
};Register the tool
import { defineToolkit } from "@assistant-ui/react";
import { z } from "zod";
import { ReadDocumentToolUI } from "@/components/assistant-ui/elements/read-document-tool-ui";
export const toolkit = defineToolkit({
read_document: {
type: "frontend",
description: "Open a document and return the passages that answer the question.",
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => lookupDocument(query),
render: ReadDocumentToolUI,
},
});import { AssistantRuntimeProvider, AuiConfig, Tools } from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/ai-sdk";
import { toolkit } from "./toolkit";
export function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
const runtime = useChatRuntime();
const config = AuiConfig({ tools: Tools({ toolkit }) });
return (
<AssistantRuntimeProvider runtime={runtime} config={config}>
{children}
</AssistantRuntimeProvider>
);
}See Tool UI for backend-defined tools and approval gates.
Standalone, the element is fully controlled: you hold the anchor list and the active page, and it reports jumps back to you.
Hold the active page
"use client";
import { useState } from "react";
import { DocumentReference } from "@/components/assistant-ui/elements/document-reference";
const ANCHORS = [
{ page: 4, quote: "Composer state moved off the thread runtime in 0.14." },
{ page: 9, quote: "Existing drafts migrate on first read; nothing to run by hand." },
];
export function Citation() {
const [activePage, setActivePage] = useState(ANCHORS[0].page);
return (
<DocumentReference
title="migration-0.14.md"
pages={12}
anchors={ANCHORS}
activePage={activePage}
onJump={setActivePage}
/>
);
}Replace the anchor list
Opening a different document swaps the whole set and resets the active page to its first anchor.
async function openDocument(id: string) {
const doc = await fetchDocument(id);
setTitle(doc.title);
setPages(doc.pages);
setAnchors(doc.anchors);
setActivePage(doc.anchors[0]?.page ?? 1);
}Anatomy
<div data-slot="document-reference">
<div>
<span>{/* file icon in a tinted square */}</span>
<div>
<span>{title}</span>
<span>{/* "N pages · M cited" */}</span>
</div>
</div>
<div>
{/* one button per anchor: "p. N" above the quoted passage */}
</div>
</div>activePage decides two independent things. Every anchor whose page equals activePage gets the active background, which can be more than one button when several anchors cite the same page. Only the first anchor in array order at that page gets aria-current, since it is found by anchors.findIndex. When activePage matches no anchor, no button gets either. Clicking a button calls onJump with that anchor's page, not its array index, so two same-page anchors are indistinguishable to the caller once clicked. An empty anchors array renders only the header, with 0 cited in the meta line and no placeholder row.
Examples
Multiple citations on the same page
Anchors don't have to be unique per page. Two entries citing page 4 both highlight when activePage is 4; only the first one in the array is marked current for assistive tech.
const anchors = [
{ page: 4, quote: "The runtime owns branch state; nothing local to track." },
{ page: 4, quote: "Reloading a message creates a sibling branch automatically." },
];Restyle the card
Both lanes take className on the root, which reads the shared paper token. The active anchor's background reads field, and the meta line and each anchor's page label read mono, both in surfaces.tsx.
<DocumentReference className="max-w-none gap-4" /* ... */ />API reference
Tool-call render props
| Prop | Type | Description |
|---|---|---|
args | TArgs | Parsed arguments. Partial while the model is still streaming them. |
argsText | string | Raw JSON argument text streamed by the model. |
result | TResult | undefined | The tool's return value once it completes. undefined while running. |
status | ToolCallMessagePartStatus | status.type is "running", "requires-action", "complete", or "incomplete". |
toolName | string | Name of the tool the model called. |
toolCallId | string | Stable id for this invocation. |
isError | boolean | undefined | Whether result represents a tool execution error. |
Register the renderer on a toolkit entry's render field and attach the toolkit with Tools({ toolkit }). See Tool UI for the full render-prop surface, including addResult, human tools, and approval gates.
DocumentReference
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | Heading text. Shadows the native title attribute, which is dropped from the forwarded props. |
pages | number | required | Total page count, shown in the header meta line. Not cross-checked against anchors. |
anchors | readonly DocumentAnchor[] | required | The citations to list, in order. |
activePage | number | required | Page number to highlight. May match zero, one, or several anchors. |
onJump | (page: number) => void | Called with an anchor's page when its button is pressed. | |
className | string | Merged onto the root. |
DocumentAnchor
| Field | Type | Description |
|---|---|---|
page | number | Page number this anchor cites. |
quote | string | The quoted passage shown under the page number. |
All other div props are forwarded to the root, except title, which the component's own prop replaces.