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.

Where the request runs: choose local or a proxy

PdfAiAssistant sends its fetch from wherever you construct it — in an Angular app, that is the user's browser. Pointing endpoint directly at a hosted cloud LLM (OpenAI, Azure OpenAI, …) from the browser has two problems:

  • Your API key is exposed. Anything shipped to the browser is readable by whoever opens devtools. A long-lived cloud key in client code is a leak, not a secret.
  • CORS will usually block the call anyway. Hosted LLM APIs don't return the Access-Control-Allow-Origin headers a browser requires for cross-origin requests, so the fetch fails before it ever reaches the model. (Azure OpenAI and OpenAI both reject direct browser calls.)

So pick one of these — don't call a cloud provider straight from the browser:

Setupendpoint points atKey locationBest for
Local modela local runtime — Ollama, LM Studionone neededprivate/offline use; the PDF text never leaves the machine. Start the runtime with browser origins allowed (e.g. Ollama's OLLAMA_ORIGINS).
Your backend proxy (recommended for cloud models)a small route on your server that forwards to the providerserver-side only, never in the browserproduction with a hosted model. The proxy injects the secret key, adds CORS, and can authenticate/rate-limit per user.

A proxy is tiny — accept the request, attach your secret key, forward to the provider, return the response:

// POST /api/pdf-chat — your server, key stays here
app.post('/api/pdf-chat', async (req, res) => {
const r = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_KEY}` },
body: JSON.stringify(req.body),
});
res.status(r.status).send(await r.text());
});
// browser — no key in sight
new PdfAiAssistant({ endpoint: '/api/pdf-chat' });

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
apiKeystringSent as Authorization: Bearer <apiKey> when provided
modelstringModel 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 and CORS. The apiKey is sent only as an Authorization: Bearer header to the endpoint you configured — nothing else sees it. But this is client-side code: any key shipped to the browser is visible to that browser's user, and most hosted providers also block direct browser calls via CORS. Don't point endpoint straight at a cloud LLM from the browser — use a local model or a backend proxy. See Where the request runs.

Embedded mode only. The built-in panel is not rendered when the viewer runs in external-window mode.