UNPKG

ng2-pdfjs-viewer

Version:

The most comprehensive Angular PDF viewer, powered by Mozilla PDF.js 6 — view, annotate, sign, fill forms, search, and read aloud from one component. 8.3M+ downloads, mobile-first, production-ready.

182 lines (179 loc) 7.4 kB
// Secondary entry point: ng2-pdfjs-viewer/ai // // A minimal bring-your-own-endpoint AI client for chat-with-PDF and // summarization flows. The library NEVER talks to any AI service on its own: // this class only sends requests when the host application calls it, to the // endpoint the host application configured, with the host application's key. // // Works with any OpenAI-compatible chat-completions endpoint: OpenAI, Azure // OpenAI, Anthropic-compatible gateways, Ollama, vLLM, LM Studio, etc. // // This is a headless, framework-free entry point — it imports nothing from // Angular, so it can be used outside an Angular context (a worker, a Node // service, a plain script). The full component re-exports these symbols from // the package root for backwards compatibility. // // SECURITY: this sends a fetch from wherever you construct it — in a browser // app, that is the user's browser. Don't point `endpoint` directly at a hosted // cloud LLM (OpenAI, Azure, …): the API key would be exposed to the client, and // most providers block direct browser calls via CORS. Use a local model // (Ollama, LM Studio) or your own backend proxy. See the AI Assistant guide. // // Usage: // import { PdfAiAssistant } from "ng2-pdfjs-viewer/ai"; // const text = await viewer.getDocumentText(); // const ai = new PdfAiAssistant({ // endpoint: "http://localhost:11434/v1/chat/completions", // e.g. Ollama // model: "llama3.2", // }); // const answer = await ai.ask("What is this document about?", text); class PdfAiAssistant { config; constructor(config) { if (!config?.endpoint) { throw new Error("PdfAiAssistant requires an endpoint"); } this.config = config; } /** * Ask a free-form question about the document. `documentText` is the output * of PdfJsViewerComponent.getDocumentText(); `history` carries prior turns * for multi-turn chat. */ async ask(question, documentText, history = [], signal, onToken) { const context = this.buildContext(documentText); const messages = [ { role: "system", content: "You answer questions about the provided PDF document. " + "Cite page numbers like [p.3] when referencing content. " + "If the answer is not in the document, say so.\n\n" + `DOCUMENT:\n${context}`, }, ...history, { role: "user", content: question }, ]; return this.complete(messages, signal, onToken); } /** One-shot document summary. */ async summarize(documentText) { return this.ask("Summarize this document concisely. Lead with what it is, then the key points.", documentText); } /** * Raw chat-completions call for custom prompting. Pass `onToken` to stream the * answer token-by-token (the callback receives the running full text and the * latest delta); the Promise still resolves to the complete text. Streaming is * requested only when `onToken` is given and `config.stream !== false`, and it * falls back to a single JSON response if the endpoint doesn't stream. */ async complete(messages, signal, onToken) { const headers = { "Content-Type": "application/json", ...(this.config.headers ?? {}), }; if (this.config.apiKey) { headers["Authorization"] = `Bearer ${this.config.apiKey}`; } const wantStream = !!onToken && this.config.stream !== false; const response = await fetch(this.config.endpoint, { method: "POST", headers, signal, body: JSON.stringify({ model: this.config.model, temperature: this.config.temperature ?? 0.2, messages, ...(wantStream ? { stream: true } : {}), }), }); if (!response.ok) { const body = await response.text().catch(() => ""); throw new Error(`AI endpoint returned ${response.status}: ${body.slice(0, 300)}`); } const contentType = response.headers?.get?.("Content-Type") ?? ""; if (wantStream && response.body && contentType.includes("text/event-stream")) { return this.readStream(response, onToken); } // Non-streaming response, or the endpoint ignored `stream`: one JSON body. const json = await response.json(); const content = json?.choices?.[0]?.message?.content; if (typeof content !== "string") { throw new Error("AI endpoint returned an unexpected response shape"); } // Emit once so callers wired for streaming still receive their update. if (onToken) onToken(content, content); return content; } /** * Read an OpenAI-style Server-Sent Events stream, accumulating * choices[0].delta.content and emitting each delta through onToken. Returns the * full concatenated text. Tolerates chunk boundaries that split SSE lines, and * skips frames it can't parse (keep-alive comments, non-`data:` lines) rather * than failing the whole stream. */ async readStream(response, onToken) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; let full = ""; const handleLine = (raw) => { const line = raw.trim(); if (!line.startsWith("data:")) return false; const data = line.slice(5).trim(); if (data === "[DONE]") return true; try { const delta = JSON.parse(data)?.choices?.[0]?.delta?.content; if (typeof delta === "string" && delta) { full += delta; onToken(full, delta); } } catch { // Ignore an unparseable frame rather than aborting the stream. } return false; }; try { for (;;) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); let nl; while ((nl = buffer.indexOf("\n")) !== -1) { const line = buffer.slice(0, nl); buffer = buffer.slice(nl + 1); if (handleLine(line)) return full; // [DONE] } } if (buffer.trim()) handleLine(buffer); // trailing line without a newline } finally { reader.releaseLock(); } return full; } buildContext(documentText) { const max = this.config.maxContextChars ?? 100_000; let out = ""; for (const page of documentText ?? []) { const chunk = `[page ${page.page}]\n${page.text}\n\n`; if (out.length + chunk.length > max) { out += `\n[truncated at ${max} characters]`; break; } out += chunk; } return out; } } /** * Generated bundle index. Do not edit. */ export { PdfAiAssistant }; //# sourceMappingURL=ng2-pdfjs-viewer-ai.mjs.map