Skip to main content

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/completions endpoint: 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 fetch to 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​

OptionTypeDefaultDescription
endpointstringβ€” (required)Full URL of an OpenAI-compatible /chat/completions endpoint
apiKeystringβ€”Sent as Authorization: Bearer <apiKey> when provided
modelstringβ€”Model name passed in the request body
headersRecord<string, string>β€”Extra request headers (e.g. Azure's api-key)
maxContextCharsnumber100000Cap on document characters included in the prompt
temperaturenumber0.2Sampling temperature
titlestring'Ask this document'Panel header text
placeholderstring'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?) β€” returns Promise<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, minus title/placeholder; throws if endpoint is missing
    • ask(question, documentText, history?, signal?) β€” answers a question about the document; history carries prior PdfAiMessage turns for multi-turn chat, signal is an AbortSignal for cancellation
    • summarize(documentText) β€” one-shot document summary
    • complete(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.