Skip to content

Commit a7c23e3

Browse files
Merge pull request #84 from ApryseSDK/new-webviewer-ai-redaction-as208
webviewer-redaction-ai sample, PR reviewed and approved.
2 parents 8f5a1f1 + cd7248a commit a7c23e3

30 files changed

Lines changed: 3736 additions & 1 deletion

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ Samples showing how to get started with WebViewer in different environments.
5252
### 3rd Party Integrations
5353
Samples showing how to integrate and use WebViewer in 3rd party platforms.
5454

55-
- [webviewer-ask-ai](./webviewer-ask-ai) - Integrate WebViewer with Artificial Intelligence
5655
- [webviewer-salesforce](./webviewer-salesforce) - Integrate WebViewer in Salesforce
5756
- [webviewer-salesforce-attachments](./webviewer-salesforce-attachments) - View Salesforce record attachments in WebViewer
5857
- [webviewer-mendix](./webviewer-mendix) - Integrate WebViewer into a Mendix low-code app
@@ -80,3 +79,9 @@ Samples showing how to use various WebViewer features.
8079
- [webviewer-range-request](./webviewer-range-request) - Setup range requests on the backend server for loading linearized PDFs in the WebViewer
8180
- [webviewer-react-canvasToPDF](./webviewer-react-canvasToPDF) - Export a canvas to PDF with WebViewer
8281

82+
### Artificial Intelligence
83+
Samples showing how to integrate WebViewer with Artificial Intelligence.
84+
85+
- [webviewer-ask-ai](./webviewer-ask-ai) - Enable chat-based Q&A, document and selected-text summarization, keyword extraction, and contextual prompts that lets users ask questions about their PDFs
86+
- [webviewer-redaction-ai](./webviewer-redaction-ai) - Identify and apply redaction of the personal information in the provided PDF
87+

lerna.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"webviewer-react",
3232
"webviewer-react-canvasToPDF",
3333
"webviewer-realtime-collaboration-sqlite3",
34+
"webviewer-redaction-ai",
3435
"webviewer-server-side-search",
3536
"webviewer-svelte",
3637
"webviewer-tomcat-java",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
DOTENV_CONFIG_QUIET=true
2+
3+
# OpenAI Configuration
4+
OPENAI_API_KEY=your-openai-api-key-here
5+
OPENAI_MODEL=your-openai-model-here
6+
OPENAI_MAX_TOKENS=your-openai-max-tokens-here
7+
OPENAI_TEMPERATURE=your-openai-temperature-here
8+
9+
# Server Configuration
10+
PORT=4040

webviewer-redaction-ai/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Misc
2+
.DS_Store
3+
node_modules
4+
5+
# WebViewer
6+
client/lib
7+
client/license-key.js

webviewer-redaction-ai/LICENSE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Copyright (c) 2026 Apryse Software Inc. All Rights Reserved.
2+
WebViewer UI project/codebase or any derived works is only permitted in solutions with an active commercial Apryse WebViewer license. For exact licensing terms refer to the commercial WebViewer license. For licensing, pricing, or product questions, Contact [Sales](https://apryse.com/form/contact-sales).

webviewer-redaction-ai/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# WebViewer - Redaction AI sample
2+
3+
Add an AI-powered assistant to WebViewer, detect Personally Identifiable Information (PII) in the provided PDF, and apply redaction to the identified information.
4+
5+
[WebViewer](https://apryse.com/products/webviewer) is a powerful JavaScript-based PDF Library that is part of the [Apryse SDK](https://apryse.com/).
6+
7+
- [WebViewer Documentation](https://docs.apryse.com/web/guides/get-started)
8+
- [WebViewer Demo](https://showcase.apryse.com/)
9+
10+
<video width="100%" autoplay loop muted playsinline>
11+
<source src="./sample.mp4" type="video/mp4">
12+
</video>
13+
14+
## Get Started
15+
16+
A license key is required to run WebViewer. You can obtain a trial key in our [get started guides](https://docs.apryse.com/web/guides/get-started), or by signing-up on our [developer portal](https://dev.apryse.com/).
17+
18+
## Initial setup
19+
20+
Before you begin, make sure the development environment includes [Node.js](https://nodejs.org/en/).
21+
22+
## Install
23+
24+
```
25+
git clone --depth=1 https://github.com/ApryseSDK/webviewer-samples.git
26+
cd webviewer-samples/webviewer-redaction-ai
27+
npm install
28+
```
29+
30+
## Configuration
31+
32+
OpenAI is the default backend for this sample. To get started, rename `.env.example` file into `.env` and fill the following:
33+
34+
```
35+
OPENAI_API_KEY=your-openai-api-key-here
36+
OPENAI_MODEL=your-openai-model-here
37+
OPENAI_MAX_TOKENS=your-openai-max-tokens-here
38+
OPENAI_TEMPERATURE=your-openai-temperature-here
39+
```
40+
41+
To use another model, replace the LangChain provider in [server/llmManager.js](https://github.com/ApryseSDK/webviewer-samples/blob/main/webviewer-redaction-ai/server/llmManager.js#L23), install the corresponding provider package, and update the .env variables for that model provider.
42+
43+
## Run
44+
45+
```
46+
npm start
47+
```
48+
49+
This will start a server that you can access the WebViewer client at http://localhost:4040/client/index.html, and manage the connection to the OpenAI on backend.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// This file is used to mock the server responses for document analysis in testing scenarios.
2+
3+
export const MOCK_DATA = {
4+
documentText: 'Peady, Eff, & Wright Exporting\n18th Floor, 2822 Glenoaks St.\nMargaritaville, QJ\nPhone 291 555 5555\nINVOICE\nINVOICE #398\nDATE: 4/1/2018\nTO:\nHarry Styles\nAce Deekay Shipping\n8710 Oakarum Bay\nDotjayess, EH\n427 555 5555\nCOMMENTS OR SPECIAL INSTRUCTIONS:\nPLEASE PAY ON RECEIPT OF INVOICE\nSALESPERSON\nP.O. NUMBER\nMary B. Dey\nQUANTITY\n1\n2\n20 boxes\n1\nLightweight Lawn Chair\nHelium Canister (2 cubic meters)\nHigh-strength Balloons\nSelfie-stick\n837626\nREQUISITIONER\nDamian Nowpleese\nDESCRIPTION\nSHIPPED VIA\nSpeedy Delivery\nF.O.B. POINT\nN/A\nUNIT PRICE\n89.99\n199.99\n5.00\n27.99\nTERMS\nDue on receipt\nTOTAL\n89.99\n399.98\n100.00\n27.99\nSUBTOTAL\nSALES TAX\nSHIPPING & HANDLING\nTOTAL DUE\nMake all checks payable to Peady, Eff, & Wright Exporting.\nIf you have any questions concerning this invoice, contact: us at 291 555 5555 or info@peadyeffwrighters.com.\n617.96\n30.90\n45.00\n693.86\nTHANK YOU FOR YOUR BUSINESS!\nCarrie Underwood: 1234-5678-3456-8494\nJohn Smith: 8374-3938-3198-0989\nEdward Harrison: 8937-1834-0934-1789\nMary Wang: 2233-7987-1235-9087\nLisa Fay: 6753-2654-0988-1123\nBen Franklin: 4455-346543-31003',
5+
analysisText: '18th Floor, 2822 Glenoaks St.\nMargaritaville, QJ\nHarry Styles\n291 555 5555\n8710 Oakarum Bay\nDotjayess, EH\n427 555 5555\nMary B. Dey\nDamian Nowpleese\ninfo@peadyeffwrighters.com\nCarrie Underwood\n1234-5678-3456-8494\nJohn Smith\n8374-3938-3198-0989\nEdward Harrison\n8937-1834-0934-1789\nMary Wang\n2233-7987-1235-9087\nLisa Fay\n6753-2654-0988-1123\nBen Franklin\n4455-346543-31003'
6+
};
7+
8+
// Utility function to safely parse JSON
9+
// returning an empty object on failure.
10+
const safeJsonParse = (value) => {
11+
try {
12+
return JSON.parse(value || '{}');
13+
} catch {
14+
return {};
15+
}
16+
};
17+
18+
// Helper function to create a JSON
19+
// response for route fulfillment.
20+
const jsonResponse = (payload) => ({
21+
status: 200,
22+
contentType: 'application/json',
23+
body: JSON.stringify(payload)
24+
});
25+
26+
// Registers API route mocks for the AI PII redaction tool tests.
27+
export const registerApiRouteMocks = async (page) => {
28+
const calls = {
29+
sendText: 0,
30+
analyzePII: 0,
31+
getResults: 0,
32+
sendTextPayload: null,
33+
analyzePIIPayload: null
34+
};
35+
36+
// Mocking the send-text endpoint to capture document text and return a success response.
37+
await page.route('**/api/send-text', async (route) => {
38+
calls.sendText += 1;
39+
calls.sendTextPayload = safeJsonParse(route.request().postData());
40+
41+
await route.fulfill(jsonResponse({
42+
success: true,
43+
message: 'Document text received successfully',
44+
textLength: calls.sendTextPayload.documentText?.length || 0,
45+
pageCount: calls.sendTextPayload.documentText?.pageCount || 0
46+
}));
47+
});
48+
49+
// Mocking the analyze-pii endpoint to capture analysis requests and return a success response.
50+
await page.route('**/api/analyze-pii', async (route) => {
51+
calls.analyzePII += 1;
52+
calls.analyzePIIPayload = safeJsonParse(route.request().postData());
53+
54+
await route.fulfill(jsonResponse({
55+
success: true,
56+
message: 'PII analysis completed'
57+
}));
58+
});
59+
60+
// Mocking the get-results endpoint to return the analysis results.
61+
await page.route('**/api/get-results', async (route) => {
62+
calls.getResults += 1;
63+
await route.fulfill(jsonResponse({
64+
success: true,
65+
message: 'Document analyzed successfully with AI',
66+
aiProcessing: true,
67+
analysis: MOCK_DATA.analysisText
68+
}));
69+
});
70+
71+
return calls;
72+
};
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// This file contains end-to-end tests for the AI PII redaction tool in WebViewer.
2+
// It uses Playwright for testing the UI interactions and validating the functionality
3+
// of the AI PII redaction tool.
4+
5+
import { test, expect } from '@playwright/test';
6+
import { MOCK_DATA, registerApiRouteMocks } from '../../__mocks__/webviewer-redaction-ai.mock.js';
7+
8+
let mockCalls = null;
9+
10+
// Before each test, we set up the API route mocks to intercept and
11+
// simulate backend responses for the AI PII redaction workflow.
12+
// This allows us to test the UI interactions and logic without
13+
// relying on actual backend services.
14+
test.beforeEach(async ({ page }) => {
15+
mockCalls = await registerApiRouteMocks(page);
16+
});
17+
18+
const normalizeMultilineText = (value) =>
19+
(value || '')
20+
.split('\n')
21+
.map((line) => line.trimEnd())
22+
.filter((line) => line !== '')
23+
.join('\n');
24+
25+
// Validating that the AI PII redaction tool button exists in the DOM.
26+
test('PII redaction tool button visibility', async ({ page }) => {
27+
// Go to the app page (adjust the URL if needed)
28+
await page.goto('/client/index.html');
29+
30+
// Click the Redact tab to activate the Redact toolbar group.
31+
await page.locator('button[data-element="toolbarGroup-Redact"]').click();
32+
33+
// Locate the AIPIIRedactionToolButton button in ModularHeader
34+
let component = page.locator('button[data-element="AIPIIRedactionToolButton"]');
35+
await expect(component).toHaveCount(1);
36+
await expect(component).toBeVisible();
37+
});
38+
39+
// Applying AI PII redaction with mocked endpoint responses.
40+
test('Perform AI PII redaction for document text size and page count within limits', async ({ page }) => {
41+
await page.goto('/client/index.html');
42+
43+
// Click the Redact tab to activate the Redact toolbar group.
44+
await page.locator('button[data-element="toolbarGroup-Redact"]').click();
45+
46+
// Ensure full document text is loaded before triggering analysis requests.
47+
await expect.poll(
48+
() => page.evaluate(() => globalThis.loadedDocument?.text || '')
49+
).toContain('Peady, Eff, & Wright Exporting');
50+
51+
// Ensure full document text length/size is within limits before triggering analysis requests.
52+
await expect.poll(
53+
() => page.evaluate(() => globalThis.loadedDocument?.text.length || 0)
54+
).toBeLessThanOrEqual(30000);
55+
56+
// Ensure page count is within limits before triggering analysis requests.
57+
await expect.poll(
58+
() => page.evaluate(() => globalThis.loadedDocument?.pageCount || 0)
59+
).toBeLessThanOrEqual(20);
60+
61+
// Trigger analyzeDocumentForPII + applyRedactions through the UI button.
62+
const pIIBtn = page.locator('button[data-element="AIPIIRedactionToolButton"]');
63+
await pIIBtn.click();
64+
65+
// Verify mocked endpoint workflow for send -> analyze -> get result.
66+
await expect.poll(() => mockCalls.sendText).toBe(1);
67+
await expect.poll(() => mockCalls.analyzePII).toBe(1);
68+
await expect.poll(() => mockCalls.getResults).toBe(1);
69+
await expect.poll(() => normalizeMultilineText(mockCalls.sendTextPayload?.documentText)).toBe(
70+
normalizeMultilineText(MOCK_DATA.documentText)
71+
);
72+
73+
// Complete the redaction flow and assert it closes cleanly.
74+
const redactAllBtn = page.locator('button[data-element="redactAllMarkedButton"]');
75+
await expect(redactAllBtn).toBeVisible();
76+
await redactAllBtn.click();
77+
78+
// Confirm the apply redaction modal and ensure it disappears after confirming.
79+
const applyRedactionBtn = page.locator('button[data-element="WarningModalSignButton"]');
80+
await expect(applyRedactionBtn).toBeVisible();
81+
await applyRedactionBtn.click();
82+
await expect(applyRedactionBtn).toBeHidden();
83+
84+
// Ensure mocked analysis text is the value used by the UI workflow.
85+
await expect.poll(() => page.evaluate(() => globalThis.aiAnalysisResult?.analysis)).toBe(MOCK_DATA.analysisText);
86+
});
87+
88+
// Expect an alert when analysis completes but /api/get-results returns no PII findings.
89+
test('Expect an alert when loaded document has no identifiable PII', async ({ page }) => {
90+
await page.goto('/client/index.html');
91+
92+
// Click the Redact tab to activate the Redact toolbar group.
93+
await page.locator('button[data-element="toolbarGroup-Redact"]').click();
94+
95+
// Wait until document wrapper is initialized before replacing the loaded text.
96+
await expect.poll(() => page.evaluate(() => Boolean(globalThis.loadedDocument))).toBe(true);
97+
98+
const noPIIText = 'Quarterly operations summary for warehouse inventory and packaging materials.';
99+
await page.evaluate((text) => {
100+
globalThis.loadedDocument.text = text;
101+
globalThis.loadedDocument.pageCount = 1;
102+
}, noPIIText);
103+
104+
let getResultsCalls = 0;
105+
await page.unroute('**/api/get-results');
106+
await page.route('**/api/get-results', async (route) => {
107+
getResultsCalls += 1;
108+
await route.fulfill({
109+
status: 200,
110+
contentType: 'application/json',
111+
body: JSON.stringify({
112+
success: false,
113+
error: 'No analysis results found. Please analyze the document first.'
114+
})
115+
});
116+
});
117+
118+
// Trigger analyzeDocumentForPII through the UI button and verify the alert content.
119+
const pIIBtn = page.locator('button[data-element="AIPIIRedactionToolButton"]');
120+
const alertPromise = page.waitForEvent('dialog');
121+
await pIIBtn.click();
122+
const alertDialog = await alertPromise;
123+
expect(alertDialog.message()).toBe('No PII result found.');
124+
await alertDialog.accept();
125+
126+
// Ensure the no-PII document text was submitted and the flow reached /api/get-results.
127+
await expect.poll(() => mockCalls.sendText).toBe(1);
128+
await expect.poll(() => mockCalls.analyzePII).toBe(1);
129+
await expect.poll(() => getResultsCalls).toBe(1);
130+
await expect.poll(() => normalizeMultilineText(mockCalls.sendTextPayload?.documentText)).toBe(noPIIText);
131+
});
132+
133+
// Expect an alert when document text/pages values exceed limits.
134+
test('Expect an alert when document text exceeds 30000 characters with page count above 20', async ({ page }) => {
135+
await page.goto('/client/index.html');
136+
137+
// Click the Redact tab to activate the Redact toolbar group.
138+
await page.locator('button[data-element="toolbarGroup-Redact"]').click();
139+
140+
// Wait until document wrapper is initialized before mutating fields.
141+
await expect.poll(() => page.evaluate(() => Boolean(globalThis.loadedDocument))).toBe(true);
142+
143+
await page.evaluate(() => {
144+
globalThis.loadedDocument.text = 'A'.repeat(30001);
145+
globalThis.loadedDocument.pageCount = 21;
146+
});
147+
148+
let sendTextCalls = 0;
149+
await page.unroute('**/api/send-text');
150+
await page.route('**/api/send-text', async (route) => {
151+
sendTextCalls += 1;
152+
await route.fulfill({
153+
status: 200,
154+
contentType: 'application/json',
155+
body: JSON.stringify({
156+
success: false,
157+
error: 'Document text size exceeds 30000 characters limit.'
158+
})
159+
});
160+
});
161+
162+
// Trigger analyzeDocumentForPII through the UI button and verify the alert content.
163+
const pIIBtn = page.locator('button[data-element="AIPIIRedactionToolButton"]');
164+
const alertPromise = page.waitForEvent('dialog');
165+
await pIIBtn.click();
166+
const alertDialog = await alertPromise;
167+
expect(alertDialog.message()).toBe('Document text size exceeds 30000 characters limit.');
168+
await alertDialog.accept();
169+
170+
// Ensure request flow stops at /api/send-text when request validation fails.
171+
await expect.poll(() => sendTextCalls).toBe(1);
172+
await expect.poll(() => mockCalls.analyzePII).toBe(0);
173+
await expect.poll(() => mockCalls.getResults).toBe(0);
174+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"scripts": {
3+
"test": "playwright test"
4+
},
5+
"devDependencies": {
6+
"@playwright/test": "^1.43.1"
7+
}
8+
}
Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)