ng2-pdfjs-viewer 26.1.0 → 26.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -16,8 +16,12 @@
|
|
|
16
16
|
[](https://github.com/mozilla/pdf.js)
|
|
17
17
|
[](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/LICENSE)
|
|
18
18
|
[](https://github.com/intbot/ng2-pdfjs-viewer)
|
|
19
|
+
[](https://stackblitz.com/github/intbot/ng2-pdfjs-viewer/tree/master/examples/quickstart)
|
|
20
|
+
[](https://codesandbox.io/p/sandbox/github/intbot/ng2-pdfjs-viewer/tree/master/examples/quickstart)
|
|
19
21
|
|
|
20
|
-
[**
|
|
22
|
+
[**Documentation**](https://angularpdf.com/) · [**API reference**](https://angularpdf.com/docs/api/component-inputs) · [**Live demo**](https://demo.angularpdf.com/) · [**Showcase**](https://angularpdf.com/showcase) · [**Changelog**](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/CHANGELOG.md)
|
|
23
|
+
|
|
24
|
+
⭐ **Find it useful? [Star it on GitHub](https://github.com/intbot/ng2-pdfjs-viewer)** — it helps more Angular developers discover it.
|
|
21
25
|
|
|
22
26
|
</div>
|
|
23
27
|
|
|
@@ -31,6 +35,9 @@ AI assistant — all driven by typed `@Input()`s and `@Output()` events, no ifra
|
|
|
31
35
|
Shipping since **2018**, **8.3+ million downloads**, mobile-first, and built & verified on **Angular 22**
|
|
32
36
|
while keeping a wide `>=10` peer range so existing apps upgrade without churn.
|
|
33
37
|
|
|
38
|
+
From France's data-protection regulator to Switzerland's federal tech institute, it's in
|
|
39
|
+
production on five continents — [see who's using it ↓](#-used-in-production).
|
|
40
|
+
|
|
34
41
|
```bash
|
|
35
42
|
npm install ng2-pdfjs-viewer
|
|
36
43
|
```
|
|
@@ -41,6 +48,22 @@ npm install ng2-pdfjs-viewer
|
|
|
41
48
|
|
|
42
49
|
That's the whole integration. [Wire up the assets](#-quick-start) and you have a full viewer.
|
|
43
50
|
|
|
51
|
+
## 🌍 Used in production
|
|
52
|
+
|
|
53
|
+
National regulators, public universities, research infrastructure, and fintech platforms render
|
|
54
|
+
PDFs with ng2-pdfjs-viewer — on five continents. Among them:
|
|
55
|
+
|
|
56
|
+
| | |
|
|
57
|
+
|---|---|
|
|
58
|
+
| <img src="https://flagcdn.com/20x15/ch.png" width="20" alt="Switzerland"> **EPFL** | Switzerland's federal institute of technology — the Infoscience research portal |
|
|
59
|
+
| <img src="https://flagcdn.com/20x15/fr.png" width="20" alt="France"> **CNIL** | France's national data-protection authority |
|
|
60
|
+
| <img src="https://flagcdn.com/20x15/fi.png" width="20" alt="Finland"> **Finnish National Agency for Education** | the country's open learning-materials library (AOE) |
|
|
61
|
+
| <img src="https://flagcdn.com/20x15/au.png" width="20" alt="Australia"> **AuScope** | Australia's national geoscience research infrastructure |
|
|
62
|
+
| <img src="https://flagcdn.com/20x15/es.png" width="20" alt="Spain"> **Spain's Ministry of Culture** | the Travesía cultural-heritage platform |
|
|
63
|
+
| <img src="https://flagcdn.com/20x15/us.png" width="20" alt="United States"> **University of Virginia** | the Supporting Transformative Autism Research (DRIVE) program |
|
|
64
|
+
|
|
65
|
+
Part of **8.3M+ installs** worldwide. [See the full showcase →](https://angularpdf.com/showcase)
|
|
66
|
+
|
|
44
67
|
## ✨ Highlights
|
|
45
68
|
|
|
46
69
|
| | |
|
|
@@ -52,11 +75,11 @@ That's the whole integration. [Wire up the assets](#-quick-start) and you have a
|
|
|
52
75
|
| 🤖 **AI assistant (BYO)** | Point it at any OpenAI-compatible endpoint (OpenAI, Azure, Ollama, vLLM…). Answers cite pages and click through. **The library never calls an AI service on its own.** |
|
|
53
76
|
| 🔊 **Read aloud** | Browser speech synthesis reads sentence by sentence, highlighting the spoken text and reporting progress. |
|
|
54
77
|
| 🗂️ **Organize pages** | Reorder, delete, cut/copy/paste, extract, and merge pages in the viewer's "Manage pages" panel. |
|
|
55
|
-
| 🎨 **Make it yours** | CSS-variable theming, true dark-mode page rendering, and your own Angular templates for the toolbar, sidebar, and per-page overlays. |
|
|
78
|
+
| 🎨 **Make it yours** | CSS-variable theming, true dark-mode page rendering, and your own Angular templates for the toolbar, sidebar, and per-page overlays — or go `chromeless` to hide the chrome for an embedded, pages-only view. |
|
|
56
79
|
| 🛡️ **Protect content** | Block print/download, disable selection, and stamp watermarks (honest client-side deterrence — not DRM). |
|
|
57
80
|
| ♿ **Accessible** | Screen-reader friendly, tagged-PDF aware, keyboard navigable — with a [WCAG / EAA guide](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/ACCESSIBILITY.md). |
|
|
58
81
|
|
|
59
|
-
→ Explore every feature with live code on the **[documentation site](https://
|
|
82
|
+
→ Explore every feature with live code on the **[documentation site](https://angularpdf.com/)** and **[demo](https://demo.angularpdf.com/)**.
|
|
60
83
|
|
|
61
84
|
## 🚀 Quick start
|
|
62
85
|
|
|
@@ -95,13 +118,13 @@ export class AppModule {}
|
|
|
95
118
|
|
|
96
119
|
> **Production note:** PDF.js 6 ships ES modules (`.mjs`), localization (`.ftl`), and WebAssembly
|
|
97
120
|
> (`.wasm`) assets. Make sure your web server returns the correct MIME types for them — see the
|
|
98
|
-
> [deployment guide](https://
|
|
121
|
+
> [deployment guide](https://angularpdf.com/docs/getting-started) for the
|
|
99
122
|
> nginx/IIS snippets.
|
|
100
123
|
|
|
101
124
|
## 🧩 What you can build
|
|
102
125
|
|
|
103
126
|
A few of the things the component makes one-liners. Full, runnable versions live in the
|
|
104
|
-
[feature guides](https://
|
|
127
|
+
[feature guides](https://angularpdf.com/docs/features/overview).
|
|
105
128
|
|
|
106
129
|
**Annotate, sign, and save the result**
|
|
107
130
|
|
|
@@ -157,15 +180,15 @@ The README is the front door — the deep reference lives on the docs site and s
|
|
|
157
180
|
|
|
158
181
|
| | |
|
|
159
182
|
|---|---|
|
|
160
|
-
| 🏁 [Getting started](https://
|
|
161
|
-
| 🧭 [Feature guides](https://
|
|
162
|
-
| 🔧 [API reference](https://
|
|
183
|
+
| 🏁 [Getting started](https://angularpdf.com/docs/getting-started) | Install, assets, first viewer, production deployment |
|
|
184
|
+
| 🧭 [Feature guides](https://angularpdf.com/docs/features/overview) | Annotation, forms, search, AI, read-aloud, custom UI, protection, and more |
|
|
185
|
+
| 🔧 [API reference](https://angularpdf.com/docs/api/component-inputs) | Every `@Input()`, `@Output()`, and method, with types |
|
|
163
186
|
| ♿ [Accessibility](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/ACCESSIBILITY.md) | Screen readers, tagged PDFs, keyboard nav, WCAG / EAA |
|
|
164
187
|
| 🖥️ [Server-side examples](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/Server-Side-Examples.md) | Streaming, authenticated fetch, signed URLs |
|
|
165
188
|
| 📝 [Changelog](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/CHANGELOG.md) | What changed, and behavior notes when upgrading |
|
|
166
189
|
|
|
167
190
|
The component exposes **30+ inputs**, **24+ events**, and **19+ Promise-returning methods**.
|
|
168
|
-
The [API reference](https://
|
|
191
|
+
The [API reference](https://angularpdf.com/docs/api/component-inputs) is the
|
|
169
192
|
complete, typed list.
|
|
170
193
|
|
|
171
194
|
## 🔌 Loading documents
|
|
@@ -185,7 +208,7 @@ attach credentials and track progress:
|
|
|
185
208
|
|
|
186
209
|
For large files, linearize ("fast web view") and serve with HTTP range support so the first pages
|
|
187
210
|
render before the whole document downloads. Details in the
|
|
188
|
-
[loading guide](https://
|
|
211
|
+
[loading guide](https://angularpdf.com/docs/features/loading-documents).
|
|
189
212
|
|
|
190
213
|
## 🛡️ Security
|
|
191
214
|
|
|
@@ -209,6 +232,12 @@ test.bat # build the lib, link it, and serve the demo on http://localho
|
|
|
209
232
|
See [CONTRIBUTING.md](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/CONTRIBUTING.md) for the full setup, and look for
|
|
210
233
|
[`good first issue`](https://github.com/intbot/ng2-pdfjs-viewer/labels/good%20first%20issue) to get started.
|
|
211
234
|
|
|
235
|
+
## 🏗️ Showcase
|
|
236
|
+
|
|
237
|
+
Shipped something with ng2-pdfjs-viewer? [Add it to the showcase](https://angularpdf.com/showcase) — submitted projects are listed next to other production apps using the viewer.
|
|
238
|
+
|
|
239
|
+
Using it somewhere that won't show up in public code — an internal tool, a hospital system, a government portal? I'd like to hear about it: email **codehippie1@gmail.com** with a line about what you're building. Teams in healthcare, finance, education, and public-sector software already have. (Bugs and feature requests are best filed as [an issue](https://github.com/intbot/ng2-pdfjs-viewer/issues).)
|
|
240
|
+
|
|
212
241
|
## 📄 License
|
|
213
242
|
|
|
214
243
|
[Apache-2.0 (Commons Clause)](https://github.com/intbot/ng2-pdfjs-viewer/blob/master/LICENSE). Free to use, modify, and self-host; the Commons
|
|
@@ -223,6 +252,6 @@ years by a community of contributors and 8.3+ million downloads' worth of real-w
|
|
|
223
252
|
|
|
224
253
|
<div align="center">
|
|
225
254
|
|
|
226
|
-
[Documentation](https://
|
|
255
|
+
[Documentation](https://angularpdf.com/) · [Live demo](https://demo.angularpdf.com/) · [npm](https://www.npmjs.com/package/ng2-pdfjs-viewer) · [Issues](https://github.com/intbot/ng2-pdfjs-viewer/issues)
|
|
227
256
|
|
|
228
257
|
</div>
|
|
@@ -40,7 +40,7 @@ class PdfAiAssistant {
|
|
|
40
40
|
* of PdfJsViewerComponent.getDocumentText(); `history` carries prior turns
|
|
41
41
|
* for multi-turn chat.
|
|
42
42
|
*/
|
|
43
|
-
async ask(question, documentText, history = [], signal) {
|
|
43
|
+
async ask(question, documentText, history = [], signal, onToken) {
|
|
44
44
|
const context = this.buildContext(documentText);
|
|
45
45
|
const messages = [
|
|
46
46
|
{
|
|
@@ -53,14 +53,20 @@ class PdfAiAssistant {
|
|
|
53
53
|
...history,
|
|
54
54
|
{ role: "user", content: question },
|
|
55
55
|
];
|
|
56
|
-
return this.complete(messages, signal);
|
|
56
|
+
return this.complete(messages, signal, onToken);
|
|
57
57
|
}
|
|
58
58
|
/** One-shot document summary. */
|
|
59
59
|
async summarize(documentText) {
|
|
60
60
|
return this.ask("Summarize this document concisely. Lead with what it is, then the key points.", documentText);
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Raw chat-completions call for custom prompting. Pass `onToken` to stream the
|
|
64
|
+
* answer token-by-token (the callback receives the running full text and the
|
|
65
|
+
* latest delta); the Promise still resolves to the complete text. Streaming is
|
|
66
|
+
* requested only when `onToken` is given and `config.stream !== false`, and it
|
|
67
|
+
* falls back to a single JSON response if the endpoint doesn't stream.
|
|
68
|
+
*/
|
|
69
|
+
async complete(messages, signal, onToken) {
|
|
64
70
|
const headers = {
|
|
65
71
|
"Content-Type": "application/json",
|
|
66
72
|
...(this.config.headers ?? {}),
|
|
@@ -68,6 +74,7 @@ class PdfAiAssistant {
|
|
|
68
74
|
if (this.config.apiKey) {
|
|
69
75
|
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
70
76
|
}
|
|
77
|
+
const wantStream = !!onToken && this.config.stream !== false;
|
|
71
78
|
const response = await fetch(this.config.endpoint, {
|
|
72
79
|
method: "POST",
|
|
73
80
|
headers,
|
|
@@ -76,19 +83,81 @@ class PdfAiAssistant {
|
|
|
76
83
|
model: this.config.model,
|
|
77
84
|
temperature: this.config.temperature ?? 0.2,
|
|
78
85
|
messages,
|
|
86
|
+
...(wantStream ? { stream: true } : {}),
|
|
79
87
|
}),
|
|
80
88
|
});
|
|
81
89
|
if (!response.ok) {
|
|
82
90
|
const body = await response.text().catch(() => "");
|
|
83
91
|
throw new Error(`AI endpoint returned ${response.status}: ${body.slice(0, 300)}`);
|
|
84
92
|
}
|
|
93
|
+
const contentType = response.headers?.get?.("Content-Type") ?? "";
|
|
94
|
+
if (wantStream && response.body && contentType.includes("text/event-stream")) {
|
|
95
|
+
return this.readStream(response, onToken);
|
|
96
|
+
}
|
|
97
|
+
// Non-streaming response, or the endpoint ignored `stream`: one JSON body.
|
|
85
98
|
const json = await response.json();
|
|
86
99
|
const content = json?.choices?.[0]?.message?.content;
|
|
87
100
|
if (typeof content !== "string") {
|
|
88
101
|
throw new Error("AI endpoint returned an unexpected response shape");
|
|
89
102
|
}
|
|
103
|
+
// Emit once so callers wired for streaming still receive their update.
|
|
104
|
+
if (onToken)
|
|
105
|
+
onToken(content, content);
|
|
90
106
|
return content;
|
|
91
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Read an OpenAI-style Server-Sent Events stream, accumulating
|
|
110
|
+
* choices[0].delta.content and emitting each delta through onToken. Returns the
|
|
111
|
+
* full concatenated text. Tolerates chunk boundaries that split SSE lines, and
|
|
112
|
+
* skips frames it can't parse (keep-alive comments, non-`data:` lines) rather
|
|
113
|
+
* than failing the whole stream.
|
|
114
|
+
*/
|
|
115
|
+
async readStream(response, onToken) {
|
|
116
|
+
const reader = response.body.getReader();
|
|
117
|
+
const decoder = new TextDecoder();
|
|
118
|
+
let buffer = "";
|
|
119
|
+
let full = "";
|
|
120
|
+
const handleLine = (raw) => {
|
|
121
|
+
const line = raw.trim();
|
|
122
|
+
if (!line.startsWith("data:"))
|
|
123
|
+
return false;
|
|
124
|
+
const data = line.slice(5).trim();
|
|
125
|
+
if (data === "[DONE]")
|
|
126
|
+
return true;
|
|
127
|
+
try {
|
|
128
|
+
const delta = JSON.parse(data)?.choices?.[0]?.delta?.content;
|
|
129
|
+
if (typeof delta === "string" && delta) {
|
|
130
|
+
full += delta;
|
|
131
|
+
onToken(full, delta);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Ignore an unparseable frame rather than aborting the stream.
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
for (;;) {
|
|
141
|
+
const { done, value } = await reader.read();
|
|
142
|
+
if (done)
|
|
143
|
+
break;
|
|
144
|
+
buffer += decoder.decode(value, { stream: true });
|
|
145
|
+
let nl;
|
|
146
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
147
|
+
const line = buffer.slice(0, nl);
|
|
148
|
+
buffer = buffer.slice(nl + 1);
|
|
149
|
+
if (handleLine(line))
|
|
150
|
+
return full; // [DONE]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (buffer.trim())
|
|
154
|
+
handleLine(buffer); // trailing line without a newline
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
reader.releaseLock();
|
|
158
|
+
}
|
|
159
|
+
return full;
|
|
160
|
+
}
|
|
92
161
|
buildContext(documentText) {
|
|
93
162
|
const max = this.config.maxContextChars ?? 100_000;
|
|
94
163
|
let out = "";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { computed } from '@angular/core';
|
|
2
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
3
|
+
|
|
4
|
+
// Secondary entry point: ng2-pdfjs-viewer/signals
|
|
5
|
+
//
|
|
6
|
+
// Read-only Angular signals projected from the viewer's @Output() events, for
|
|
7
|
+
// zoneless and OnPush apps that prefer reading state from signals over wiring up
|
|
8
|
+
// a handful of (event)="..." bindings and EventEmitter subscriptions.
|
|
9
|
+
//
|
|
10
|
+
// It reads the same outputs the component already emits - it adds no new viewer
|
|
11
|
+
// behaviour and sends nothing anywhere. Each signal holds the latest value from
|
|
12
|
+
// its source output and starts as `undefined` until that output first fires
|
|
13
|
+
// (`loaded`/`totalPages` are derived from page initialization).
|
|
14
|
+
//
|
|
15
|
+
// Requires Angular 16+ (signals + `@angular/core/rxjs-interop`). The base package
|
|
16
|
+
// keeps its `>=10` peer range; only this entry point needs 16+, so import it from
|
|
17
|
+
// the subpath and apps on older Angular never load it:
|
|
18
|
+
//
|
|
19
|
+
// import { pdfViewerSignals } from "ng2-pdfjs-viewer/signals";
|
|
20
|
+
//
|
|
21
|
+
// A @ViewChild viewer is only populated in ngAfterViewInit, which is NOT an
|
|
22
|
+
// injection context, so pass an Injector captured earlier:
|
|
23
|
+
//
|
|
24
|
+
// @ViewChild('viewer') viewer!: PdfJsViewerComponent;
|
|
25
|
+
// private injector = inject(Injector);
|
|
26
|
+
// signals!: PdfViewerSignals;
|
|
27
|
+
//
|
|
28
|
+
// ngAfterViewInit() {
|
|
29
|
+
// this.signals = pdfViewerSignals(this.viewer, { injector: this.injector });
|
|
30
|
+
// }
|
|
31
|
+
// // template: {{ signals.page() }} / {{ signals.totalPages() }} / {{ signals.loaded() }}
|
|
32
|
+
//
|
|
33
|
+
// Called from a constructor or field initializer (where an injection context
|
|
34
|
+
// exists), the `injector` option can be omitted. Subscriptions are torn down with
|
|
35
|
+
// the injector's DestroyRef - i.e. when the host component is destroyed.
|
|
36
|
+
/**
|
|
37
|
+
* Project the viewer's outputs into read-only signals. See the file header for
|
|
38
|
+
* usage and the injection-context rules. The returned signals are live: each
|
|
39
|
+
* updates when its source output next fires.
|
|
40
|
+
*/
|
|
41
|
+
function pdfViewerSignals(viewer, options = {}) {
|
|
42
|
+
const { injector } = options;
|
|
43
|
+
const latest = (source) => toSignal(source, { initialValue: undefined, injector });
|
|
44
|
+
const pages = latest(viewer.onPagesInit);
|
|
45
|
+
return {
|
|
46
|
+
page: latest(viewer.onPageChange),
|
|
47
|
+
scale: latest(viewer.onScaleChange),
|
|
48
|
+
totalPages: computed(() => pages()?.pagesCount),
|
|
49
|
+
loaded: computed(() => pages() !== undefined),
|
|
50
|
+
error: latest(viewer.onDocumentError),
|
|
51
|
+
rotation: latest(viewer.onRotationChange),
|
|
52
|
+
findMatches: latest(viewer.onUpdateFindMatchesCount),
|
|
53
|
+
readAloud: latest(viewer.onReadAloudStateChange),
|
|
54
|
+
annotationEditor: latest(viewer.onAnnotationEditorStateChange),
|
|
55
|
+
annotationEditorMode: latest(viewer.annotationEditorChange),
|
|
56
|
+
sidebar: latest(viewer.onSidebarViewChanged),
|
|
57
|
+
metadata: latest(viewer.onMetadataLoaded),
|
|
58
|
+
outline: latest(viewer.onOutlineLoaded),
|
|
59
|
+
formData: latest(viewer.formDataChange),
|
|
60
|
+
presentationMode: latest(viewer.onPresentationModeChanged),
|
|
61
|
+
zoom: latest(viewer.zoomChange),
|
|
62
|
+
cursor: latest(viewer.cursorChange),
|
|
63
|
+
scroll: latest(viewer.scrollChange),
|
|
64
|
+
spread: latest(viewer.spreadChange),
|
|
65
|
+
pageMode: latest(viewer.pageModeChange),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generated bundle index. Do not edit.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
export { pdfViewerSignals };
|
|
74
|
+
//# sourceMappingURL=ng2-pdfjs-viewer-signals.mjs.map
|
|
@@ -1702,6 +1702,18 @@ class PdfJsViewerComponent {
|
|
|
1702
1702
|
if (this.diagnosticLogs)
|
|
1703
1703
|
console.debug("PdfJsViewer: The document has now been loaded!");
|
|
1704
1704
|
this.onDocumentLoad.emit();
|
|
1705
|
+
// Project onPagesInit here. PDF.js fires the real 'pagesinit' once,
|
|
1706
|
+
// BEFORE this handler map finishes registering (it lands between
|
|
1707
|
+
// 'pagesinit' and 'scalechanging'), and the postMessage wrapper's own
|
|
1708
|
+
// pagesInit relay is wired on 'documentloaded' too late for the
|
|
1709
|
+
// one-shot - so onPagesInit never fired and the signals entry point's
|
|
1710
|
+
// loaded()/totalPages() stayed empty. 'documentloaded' is reliably
|
|
1711
|
+
// caught (it drives onDocumentLoad) and pagesCount is set by now.
|
|
1712
|
+
const app = this.PDFViewerApplication;
|
|
1713
|
+
const pagesCount = app?.pagesCount ?? app?.pdfDocument?.numPages;
|
|
1714
|
+
if (typeof pagesCount === "number" && pagesCount > 0) {
|
|
1715
|
+
this.onPagesInit.emit({ pagesCount });
|
|
1716
|
+
}
|
|
1705
1717
|
// Queue auto-actions with the property values current at THIS load
|
|
1706
1718
|
this.queueAutoActionsForDocumentLoad();
|
|
1707
1719
|
// Execute all queued auto-actions
|
|
@@ -2100,6 +2112,9 @@ class PdfJsViewerComponent {
|
|
|
2100
2112
|
this.aiAbort = new AbortController();
|
|
2101
2113
|
const signal = this.aiAbort.signal;
|
|
2102
2114
|
this.aiMessages.push({ role: "user", content: q, parts: [{ text: q }] });
|
|
2115
|
+
// Placeholder assistant turn we stream tokens into as they arrive.
|
|
2116
|
+
const assistant = { role: "assistant", content: "", parts: [] };
|
|
2117
|
+
this.aiMessages.push(assistant);
|
|
2103
2118
|
this.cdr.markForCheck();
|
|
2104
2119
|
try {
|
|
2105
2120
|
if (!this.aiClient || this.aiClientConfig !== config) {
|
|
@@ -2110,29 +2125,28 @@ class PdfJsViewerComponent {
|
|
|
2110
2125
|
this.aiDocText = await this.getDocumentText();
|
|
2111
2126
|
}
|
|
2112
2127
|
const history = this.aiMessages
|
|
2113
|
-
.slice(0, -
|
|
2128
|
+
.slice(0, -2)
|
|
2114
2129
|
.filter((m) => !m.error)
|
|
2115
2130
|
.map((m) => ({ role: m.role, content: m.content }));
|
|
2116
|
-
const answer = await this.aiClient.ask(q, this.aiDocText, history, signal)
|
|
2131
|
+
const answer = await this.aiClient.ask(q, this.aiDocText, history, signal, (full) => {
|
|
2132
|
+
if (generation !== this.aiGeneration) {
|
|
2133
|
+
return; // stale stream - ignore late tokens
|
|
2134
|
+
}
|
|
2135
|
+
assistant.content = full;
|
|
2136
|
+
assistant.parts = this.parseAiCitations(full);
|
|
2137
|
+
this.cdr.markForCheck();
|
|
2138
|
+
});
|
|
2117
2139
|
if (generation !== this.aiGeneration) {
|
|
2118
2140
|
return; // document changed mid-flight - stale answer
|
|
2119
2141
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
content: answer,
|
|
2123
|
-
parts: this.parseAiCitations(answer),
|
|
2124
|
-
});
|
|
2142
|
+
assistant.content = answer;
|
|
2143
|
+
assistant.parts = this.parseAiCitations(answer);
|
|
2125
2144
|
}
|
|
2126
2145
|
catch (e) {
|
|
2127
2146
|
if (generation !== this.aiGeneration) {
|
|
2128
2147
|
return; // aborted by invalidation - already cleaned up
|
|
2129
2148
|
}
|
|
2130
|
-
|
|
2131
|
-
role: "assistant",
|
|
2132
|
-
content: "",
|
|
2133
|
-
error: e?.message || "AI request failed",
|
|
2134
|
-
parts: [],
|
|
2135
|
-
});
|
|
2149
|
+
assistant.error = e?.message || "AI request failed";
|
|
2136
2150
|
}
|
|
2137
2151
|
finally {
|
|
2138
2152
|
if (generation === this.aiGeneration) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ng2-pdfjs-viewer",
|
|
3
|
-
"version": "26.
|
|
3
|
+
"version": "26.2.0",
|
|
4
4
|
"description": "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.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Aneesh Goapalakrishnan",
|
|
@@ -20,34 +20,37 @@
|
|
|
20
20
|
"@angular/core": ">=10.0.0"
|
|
21
21
|
},
|
|
22
22
|
"$schema": "./node_modules/ng-packagr/package.schema.json",
|
|
23
|
-
"homepage": "https://
|
|
23
|
+
"homepage": "https://angularpdf.com/",
|
|
24
24
|
"bugs": {
|
|
25
25
|
"url": "https://github.com/intbot/ng2-pdfjs-viewer/issues"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
28
|
+
"angular pdf viewer",
|
|
28
29
|
"angular",
|
|
29
30
|
"angular-pdf-viewer",
|
|
31
|
+
"ng2-pdfjs-viewer",
|
|
30
32
|
"pdf-viewer",
|
|
31
33
|
"pdf-reader",
|
|
32
34
|
"PDF",
|
|
33
|
-
"PDF Viewer",
|
|
34
35
|
"pdfjs",
|
|
35
|
-
"
|
|
36
|
-
"ng2",
|
|
36
|
+
"pdf.js",
|
|
37
37
|
"angular-component",
|
|
38
38
|
"angular-pdf",
|
|
39
39
|
"ng2-pdf",
|
|
40
|
-
"angular 2 - 22",
|
|
41
40
|
"angular 22",
|
|
42
41
|
"angular-library",
|
|
43
42
|
"typescript",
|
|
44
43
|
"javascript",
|
|
45
44
|
"pdf-rendering",
|
|
46
45
|
"pdf-annotations",
|
|
46
|
+
"pdf-forms",
|
|
47
|
+
"pdf-signature",
|
|
48
|
+
"esign",
|
|
49
|
+
"pdf-search",
|
|
47
50
|
"pdf-zoom",
|
|
48
51
|
"pdf-printing",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
52
|
+
"text-to-speech",
|
|
53
|
+
"mobile-first",
|
|
51
54
|
"accessibility",
|
|
52
55
|
"open-source"
|
|
53
56
|
],
|
|
@@ -64,6 +67,10 @@
|
|
|
64
67
|
"./ai": {
|
|
65
68
|
"types": "./types/ng2-pdfjs-viewer-ai.d.ts",
|
|
66
69
|
"default": "./fesm2022/ng2-pdfjs-viewer-ai.mjs"
|
|
70
|
+
},
|
|
71
|
+
"./signals": {
|
|
72
|
+
"types": "./types/ng2-pdfjs-viewer-signals.d.ts",
|
|
73
|
+
"default": "./fesm2022/ng2-pdfjs-viewer-signals.mjs"
|
|
67
74
|
}
|
|
68
75
|
},
|
|
69
76
|
"sideEffects": false,
|
|
@@ -9,6 +9,7 @@ interface PdfAiAssistantConfig {
|
|
|
9
9
|
headers?: Record<string, string>;
|
|
10
10
|
maxContextChars?: number;
|
|
11
11
|
temperature?: number;
|
|
12
|
+
stream?: boolean;
|
|
12
13
|
}
|
|
13
14
|
interface PdfAiMessage {
|
|
14
15
|
role: "system" | "user" | "assistant";
|
|
@@ -35,11 +36,25 @@ declare class PdfAiAssistant {
|
|
|
35
36
|
* of PdfJsViewerComponent.getDocumentText(); `history` carries prior turns
|
|
36
37
|
* for multi-turn chat.
|
|
37
38
|
*/
|
|
38
|
-
ask(question: string, documentText: PdfPageText[], history?: PdfAiMessage[], signal?: AbortSignal): Promise<string>;
|
|
39
|
+
ask(question: string, documentText: PdfPageText[], history?: PdfAiMessage[], signal?: AbortSignal, onToken?: (full: string, delta: string) => void): Promise<string>;
|
|
39
40
|
/** One-shot document summary. */
|
|
40
41
|
summarize(documentText: PdfPageText[]): Promise<string>;
|
|
41
|
-
/**
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Raw chat-completions call for custom prompting. Pass `onToken` to stream the
|
|
44
|
+
* answer token-by-token (the callback receives the running full text and the
|
|
45
|
+
* latest delta); the Promise still resolves to the complete text. Streaming is
|
|
46
|
+
* requested only when `onToken` is given and `config.stream !== false`, and it
|
|
47
|
+
* falls back to a single JSON response if the endpoint doesn't stream.
|
|
48
|
+
*/
|
|
49
|
+
complete(messages: PdfAiMessage[], signal?: AbortSignal, onToken?: (full: string, delta: string) => void): Promise<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Read an OpenAI-style Server-Sent Events stream, accumulating
|
|
52
|
+
* choices[0].delta.content and emitting each delta through onToken. Returns the
|
|
53
|
+
* full concatenated text. Tolerates chunk boundaries that split SSE lines, and
|
|
54
|
+
* skips frames it can't parse (keep-alive comments, non-`data:` lines) rather
|
|
55
|
+
* than failing the whole stream.
|
|
56
|
+
*/
|
|
57
|
+
private readStream;
|
|
43
58
|
private buildContext;
|
|
44
59
|
}
|
|
45
60
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { EventEmitter, Signal, Injector } from '@angular/core';
|
|
2
|
+
import { ChangedRotation, PagesInfo, DocumentError, FindMatchesCount, ReadAloudState, AnnotationEditorState, AnnotationEditorMode, SidebarViewChange, DocumentMetadata, DocumentOutline, FormDataMap, PresentationMode } from 'ng2-pdfjs-viewer';
|
|
3
|
+
|
|
4
|
+
interface PdfViewerSignalSource {
|
|
5
|
+
onPageChange: EventEmitter<number>;
|
|
6
|
+
onScaleChange: EventEmitter<number>;
|
|
7
|
+
onRotationChange: EventEmitter<ChangedRotation>;
|
|
8
|
+
onPagesInit: EventEmitter<PagesInfo>;
|
|
9
|
+
onDocumentError: EventEmitter<DocumentError>;
|
|
10
|
+
onUpdateFindMatchesCount: EventEmitter<FindMatchesCount>;
|
|
11
|
+
onReadAloudStateChange: EventEmitter<ReadAloudState>;
|
|
12
|
+
onAnnotationEditorStateChange: EventEmitter<AnnotationEditorState>;
|
|
13
|
+
annotationEditorChange: EventEmitter<AnnotationEditorMode>;
|
|
14
|
+
onSidebarViewChanged: EventEmitter<SidebarViewChange>;
|
|
15
|
+
onMetadataLoaded: EventEmitter<DocumentMetadata>;
|
|
16
|
+
onOutlineLoaded: EventEmitter<DocumentOutline>;
|
|
17
|
+
formDataChange: EventEmitter<FormDataMap>;
|
|
18
|
+
onPresentationModeChanged: EventEmitter<PresentationMode>;
|
|
19
|
+
zoomChange: EventEmitter<string>;
|
|
20
|
+
cursorChange: EventEmitter<string>;
|
|
21
|
+
scrollChange: EventEmitter<string>;
|
|
22
|
+
spreadChange: EventEmitter<string>;
|
|
23
|
+
pageModeChange: EventEmitter<string>;
|
|
24
|
+
}
|
|
25
|
+
interface PdfViewerSignals {
|
|
26
|
+
/** Current 1-based page number; undefined until the first page change. */
|
|
27
|
+
page: Signal<number | undefined>;
|
|
28
|
+
/** Current zoom scale factor (1 = 100%); undefined until the first scale change. */
|
|
29
|
+
scale: Signal<number | undefined>;
|
|
30
|
+
/** Total page count, available once the document's pages initialize. */
|
|
31
|
+
totalPages: Signal<number | undefined>;
|
|
32
|
+
/** True once the document's pages have initialized. */
|
|
33
|
+
loaded: Signal<boolean>;
|
|
34
|
+
/** Last document-load error, or undefined if none. */
|
|
35
|
+
error: Signal<DocumentError | undefined>;
|
|
36
|
+
/** Page rotation state ({ rotation, page }). */
|
|
37
|
+
rotation: Signal<ChangedRotation | undefined>;
|
|
38
|
+
/** Live find/search match counts ({ current, total }). */
|
|
39
|
+
findMatches: Signal<FindMatchesCount | undefined>;
|
|
40
|
+
/** Read-aloud progress ({ status, page, sentence? }). */
|
|
41
|
+
readAloud: Signal<ReadAloudState | undefined>;
|
|
42
|
+
/** Annotation editor undo/empty/selection state. */
|
|
43
|
+
annotationEditor: Signal<AnnotationEditorState | undefined>;
|
|
44
|
+
/** Active annotation editor tool (highlight / ink / freetext / ...). */
|
|
45
|
+
annotationEditorMode: Signal<AnnotationEditorMode | undefined>;
|
|
46
|
+
/** Current sidebar panel ({ view }). */
|
|
47
|
+
sidebar: Signal<SidebarViewChange | undefined>;
|
|
48
|
+
/** Document metadata (title / author / ...). */
|
|
49
|
+
metadata: Signal<DocumentMetadata | undefined>;
|
|
50
|
+
/** Document outline / bookmarks ({ items?, hasOutline }). */
|
|
51
|
+
outline: Signal<DocumentOutline | undefined>;
|
|
52
|
+
/** Current AcroForm field values. */
|
|
53
|
+
formData: Signal<FormDataMap | undefined>;
|
|
54
|
+
/** Presentation (fullscreen) mode ({ active }). */
|
|
55
|
+
presentationMode: Signal<PresentationMode | undefined>;
|
|
56
|
+
/** Zoom mode string (e.g. 'auto', 'page-fit', '125'). */
|
|
57
|
+
zoom: Signal<string | undefined>;
|
|
58
|
+
/** Cursor tool ('select' | 'hand'). */
|
|
59
|
+
cursor: Signal<string | undefined>;
|
|
60
|
+
/** Scroll mode ('vertical' | 'horizontal' | 'wrapped' | 'page'). */
|
|
61
|
+
scroll: Signal<string | undefined>;
|
|
62
|
+
/** Spread mode ('none' | 'odd' | 'even'). */
|
|
63
|
+
spread: Signal<string | undefined>;
|
|
64
|
+
/** Page (view) mode. */
|
|
65
|
+
pageMode: Signal<string | undefined>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Project the viewer's outputs into read-only signals. See the file header for
|
|
69
|
+
* usage and the injection-context rules. The returned signals are live: each
|
|
70
|
+
* updates when its source output next fires.
|
|
71
|
+
*/
|
|
72
|
+
declare function pdfViewerSignals(viewer: PdfViewerSignalSource, options?: {
|
|
73
|
+
injector?: Injector;
|
|
74
|
+
}): PdfViewerSignals;
|
|
75
|
+
|
|
76
|
+
export { pdfViewerSignals };
|
|
77
|
+
export type { PdfViewerSignalSource, PdfViewerSignals };
|