AI Assistant (Bring Your Own Endpoint)
Chat with the open PDF β using your AI endpoint, your key, and no vendor backend.
The most important thing to understand about this feature is what it does not do:
- The library never calls any AI service on its own. There is no bundled API key, no default endpoint, no telemetry. If you don't configure an endpoint, no AI-related network request is ever made.
- You point it at any OpenAI-compatible
/chat/completionsendpoint: OpenAI, Azure OpenAI, Ollama, vLLM, LM Studio, or any gateway/proxy that speaks the same protocol. - Everything runs client-side. The component extracts the document's text from the
PDF.js text layer, builds the prompt in the browser, and sends a single
fetchto the endpoint you configured. Your PDF text goes to your endpoint and nowhere else.
This means you can run fully private setups (a local Ollama model β the PDF text never leaves the machine) or production setups behind your own server-side proxy.
Built-in Chat Panelβ
Bind [aiAssistantConfig] to render a floating chat button in the bottom-right corner of
the viewer. Clicking it opens a chat panel where users can ask questions about the open
document.
import { PdfAiPanelConfig } from 'ng2-pdfjs-viewer';
export class MyComponent {
aiConfig: PdfAiPanelConfig = {
endpoint: 'https://api.openai.com/v1/chat/completions',
apiKey: 'sk-...', // sent as 'Authorization: Bearer <apiKey>'
model: 'gpt-4o-mini',
title: 'Ask this report',
};
}
<ng2-pdfjs-viewer
pdfSrc="assets/report.pdf"
[aiAssistantConfig]="aiConfig">
</ng2-pdfjs-viewer>
PdfAiPanelConfigβ
| Option | Type | Default | Description |
|---|---|---|---|
endpoint | string | β (required) | Full URL of an OpenAI-compatible /chat/completions endpoint |
apiKey | string | β | Sent as Authorization: Bearer <apiKey> when provided |
model | string | β | Model name passed in the request body |
headers | Record<string, string> | β | Extra request headers (e.g. Azure's api-key) |
maxContextChars | number | 100000 | Cap on document characters included in the prompt |
temperature | number | 0.2 | Sampling temperature |
title | string | 'Ask this document' | Panel header text |
placeholder | string | 'Ask the documentβ¦' | Input placeholder |
Clickable Page Citationsβ
The system prompt instructs the model to cite page numbers like [p.3] when referencing
content. The panel parses those citations out of every answer and renders them as
clickable chips β clicking p.3 jumps the viewer to page 3. This turns answers into
navigation: "Where is the termination clause?" β click the citation, land on the page.
Asking Programmaticallyβ
aiAsk(question) goes through the exact same path as the panel's input β the question and
answer appear in the panel's conversation:
@ViewChild('viewer') viewer!: PdfJsViewerComponent;
summarize(): Promise<void> {
return this.viewer.aiAsk('Summarize this document in three bullet points.');
}
aiAsk requires [aiAssistantConfig] to be set, and one question runs at a time (calls
made while a request is in flight are ignored). Document text is extracted once per
document and reused across questions.
Chat Lifecycleβ
The conversation is tied to the document. When the document changes (new pdfSrc, a
viewer reload), the chat clears automatically and any in-flight request is aborted β
a slow answer about the old document can never land in the new document's chat.
Theming the Panelβ
The panel and its floating button are styled with CSS custom properties:
ng2-pdfjs-viewer {
--ng2-ai-accent: #0d9488; /* button, header, citations, send button */
--ng2-ai-bg: #ffffff; /* panel background */
--ng2-ai-text: #222222; /* panel text color */
--ng2-ai-question-bg: #e4f0fe; /* user message bubbles */
--ng2-ai-answer-bg: #f2f1f7; /* assistant message bubbles */
--ng2-ai-fab-color: #ffffff; /* floating button icon color */
}
Headless: PdfAiAssistant + getDocumentText()β
If you want your own chat UI (or no chat at all β just a "Summarize" button), skip
[aiAssistantConfig] entirely and use the two lower-level building blocks:
viewer.getDocumentText(from?, to?)β returnsPromise<DocumentPageText[]>({ page: number; text: string }per page), the plain text of the document or a 1-based page range, extracted from the PDF.js text layer.PdfAiAssistantβ a minimal OpenAI-compatible chat client, exported from the package:constructor(config: PdfAiAssistantConfig)β same options as the panel config, minustitle/placeholder; throws ifendpointis missingask(question, documentText, history?, signal?)β answers a question about the document;historycarries priorPdfAiMessageturns for multi-turn chat,signalis anAbortSignalfor cancellationsummarize(documentText)β one-shot document summarycomplete(messages, signal?)β raw chat-completions call for fully custom prompting
Example: Local Ollama + a Custom Summarize Buttonβ
A fully private setup β the model runs on localhost, so the document text never leaves
the machine:
import { Component, ViewChild } from '@angular/core';
import { PdfJsViewerComponent, PdfAiAssistant } from 'ng2-pdfjs-viewer';
@Component({
selector: 'app-report',
templateUrl: './report.component.html',
})
export class ReportComponent {
@ViewChild('viewer') viewer!: PdfJsViewerComponent;
summary = '';
busy = false;
private ai = new PdfAiAssistant({
endpoint: 'http://localhost:11434/v1/chat/completions', // Ollama
model: 'llama3.2',
// no apiKey needed for a local model
});
async summarize(): Promise<void> {
this.busy = true;
try {
const text = await this.viewer.getDocumentText();
this.summary = await this.ai.summarize(text);
} finally {
this.busy = false;
}
}
}
<button (click)="summarize()" [disabled]="busy">
{{ busy ? 'Summarizingβ¦' : 'Summarize' }}
</button>
<p *ngIf="summary">{{ summary }}</p>
<ng2-pdfjs-viewer #viewer pdfSrc="assets/report.pdf"></ng2-pdfjs-viewer>
The same PdfAiAssistant config shape works for vLLM, LM Studio, and OpenAI-compatible
gateways β only the endpoint (and credentials) change.
Honest Caveatsβ
Context stuffing, not RAG. The assistant includes the document text directly in the
prompt as [page N] blocks, capped at maxContextChars (default 100,000 characters).
Very large documents are truncated at the cap β questions about content past the cutoff
won't be answerable. There is no chunking, embedding, or retrieval; for very large
corpora, build retrieval on top of getDocumentText() and use complete() for the
final call.
Text extraction has limits. getDocumentText() reads the PDF.js text layer, so
scanned/image-only PDFs yield little or no text β there is no OCR.
Key handling. The apiKey is sent only as an Authorization: Bearer header to the
endpoint you configured β nothing else sees it. But remember this is client-side code:
any key shipped to the browser is visible to that browser's user. For production with a
shared secret, put your key behind a server-side proxy and point endpoint at the proxy;
for fully private use, point at a local model (Ollama, LM Studio).
Embedded mode only. The built-in panel is not rendered when the viewer runs in external-window mode.
Relatedβ
- Read Aloud (Text-to-Speech) β the other no-network assistive feature
- Custom Toolbar, Sidebar & Page Overlays β build your own AI UI into a custom toolbar or sidebar
- Security Features β the viewer's broader security model
- API Reference: Methods β
aiAsk,getDocumentText, and friends