Elements

Elements · Knowledge

Document reference

A document the answer leans on, with the quoted passage and the page to jump to.

migration-0.14.pdf14 pages · 2 cited
fig. 01

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 init

Then 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.

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

components/assistant-ui/elements/read-document-tool-ui.tsx
"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

app/toolkit.ts
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,
  },
});
app/MyRuntimeProvider.tsx
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.

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

PropTypeDescription
argsTArgsParsed arguments. Partial while the model is still streaming them.
argsTextstringRaw JSON argument text streamed by the model.
resultTResult | undefinedThe tool's return value once it completes. undefined while running.
statusToolCallMessagePartStatusstatus.type is "running", "requires-action", "complete", or "incomplete".
toolNamestringName of the tool the model called.
toolCallIdstringStable id for this invocation.
isErrorboolean | undefinedWhether 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.