Use Case: AI / RAG Citations
Programmatically create ephemeral highlights anchored to AI / RAG response citations.
When an AI assistant answers a question with citations into a document, highlighting the cited passages in the viewer is a powerful UX. DocumentViewer supports this directly via createAnnotation + the metadata.is_ephemeral convention.
Pattern
async function showCitation(citation: { pageIndex: number; rect: PdfRect; id: string }) {
// Clear any previous ephemeral citation
if (currentEphemeralId.value) {
await viewer.value?.deleteAnnotation(currentEphemeralId.value)
}
const created = await viewer.value?.createAnnotation({
type: 'highlight',
pageIndex: citation.pageIndex,
rect: citation.rect,
segmentRects: [citation.rect],
color: '#92E89E', // green to distinguish from user highlights
opacity: 0.5,
metadata: {
is_ephemeral: true,
citation_id: citation.id,
},
})
currentEphemeralId.value = created?.id ?? null
if (created) viewer.value?.navigateToAnnotation(created.id)
}
Why metadata.is_ephemeral?
The metadata is free-form, but the is_ephemeral convention lets your annotation events handler skip persisting these to your backend:
function onCreate({ annotation }: { annotation: Annotation }) {
if (annotation.metadata?.is_ephemeral) return // don't save AI citations
fetch('/api/annotations', { method: 'POST', body: JSON.stringify(annotation) })
}
Otherwise every AI response would litter your database with ephemeral highlights.
Differentiating from user highlights
A common pattern is to use a different colour for AI citations than for user highlights, so users can visually distinguish “my notes” from “AI cited this”:
| Origin | Colour |
|---|---|
| User-created highlight | Yellow #FFCD45 (default) |
| AI citation | Green #92E89E or Cyan #A0E7E5 |
Combine with metadata.is_ephemeral: true and your event handler can filter both ways.
Generating citation coordinates
The hard part is getting pageIndex + rect from your AI / RAG pipeline. A typical pipeline:
- Extract text per page with PDFium server-side (or any PDF-text library) and record per-character bounding boxes.
- Index those chunks in your vector store. Each chunk has
{ text, pageIndex, rect }. - When the LLM cites a chunk, send the
pageIndex+rectback to the client. - Call
createAnnotationwith that pageIndex + rect (and a segmentRect equal to the full rect).
For longer citations spanning multiple lines, pass multiple segmentRects (one per line).
Auto-clearing on response change
When the user asks a new question, clear the prior citation:
watch(currentResponse, async (newResp) => {
if (currentEphemeralId.value) {
await viewer.value?.deleteAnnotation(currentEphemeralId.value)
currentEphemeralId.value = null
}
if (newResp?.citation) {
await showCitation(newResp.citation)
}
})