Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ Samples showing how to get started with WebViewer in different environments.
### 3rd Party Integrations
Samples showing how to integrate and use WebViewer in 3rd party platforms.

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

### Artificial Intelligence
Samples showing how to integrate WebViewer with Artificial Intelligence.

- [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
- [webviewer-redaction-ai](./webviewer-redaction-ai) - Identify and apply redaction of the personal information in the provided PDF

1 change: 1 addition & 0 deletions lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"webviewer-react",
"webviewer-react-canvasToPDF",
"webviewer-realtime-collaboration-sqlite3",
"webviewer-redaction-ai",
"webviewer-server-side-search",
"webviewer-svelte",
"webviewer-tomcat-java",
Expand Down
10 changes: 10 additions & 0 deletions webviewer-redaction-ai/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DOTENV_CONFIG_QUIET=true

# OpenAI Configuration
OPENAI_API_KEY=your-openai-api-key-here
OPENAI_MODEL=your-openai-model-here
OPENAI_MAX_TOKENS=your-openai-max-tokens-here
OPENAI_TEMPERATURE=your-openai-temperature-here

# Server Configuration
PORT=4040
7 changes: 7 additions & 0 deletions webviewer-redaction-ai/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Misc
.DS_Store
node_modules

# WebViewer
client/lib
client/license-key.js
2 changes: 2 additions & 0 deletions webviewer-redaction-ai/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Copyright (c) 2026 Apryse Software Inc. All Rights Reserved.
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).
49 changes: 49 additions & 0 deletions webviewer-redaction-ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# WebViewer - Redaction AI sample

Add an AI-powered assistant to WebViewer, identify personal information in the provided PDF, and apply redaction to the identified information.

[WebViewer](https://apryse.com/products/webviewer) is a powerful JavaScript-based PDF Library that is part of the [Apryse SDK](https://apryse.com/).

- [WebViewer Documentation](https://docs.apryse.com/web/guides/get-started)
- [WebViewer Demo](https://showcase.apryse.com/)

<video width="100%" autoplay loop muted playsinline>
<source src="./sample.mp4" type="video/mp4">
</video>

## Get Started

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/).

## Initial setup

Before you begin, make sure the development environment includes [Node.js](https://nodejs.org/en/).

## Install

```
git clone --depth=1 https://github.com/ApryseSDK/webviewer-samples.git
cd webviewer-samples/webviewer-redaction-ai
npm install
```

## Configuration

OpenAI is the default backend for this sample. To get started, rename `.env.example` file into `.env` and fill the following:

```
OPENAI_API_KEY=your-openai-api-key-here
OPENAI_MODEL=your-openai-model-here
OPENAI_MAX_TOKENS=your-openai-max-tokens-here
OPENAI_TEMPERATURE=your-openai-temperature-here
```

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.

## Run

```
npm start
```

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.
27 changes: 27 additions & 0 deletions webviewer-redaction-ai/__mocks__/webviewer-redaction-ai.mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// This file is used to mock the server responses for document analysis in testing scenarios.

// Mock responses for testing
const MOCK_RESPONSES = {
analysis: '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',
documentId: 'mock-document-id'
};

// Check if mocking mode is enabled
export const isMockingModeEnabled = () => {
if (typeof window !== 'undefined')

Check warning on line 11 in webviewer-redaction-ai/__mocks__/webviewer-redaction-ai.mock.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis.window` over `window`.

See more on https://sonarcloud.io/project/issues?id=ApryseSDK_webviewer-samples&issues=AZz6-sPo3yukrggiJdmT&open=AZz6-sPo3yukrggiJdmT&pullRequest=84
return window.MODE_ENV === 'mocking';

Check warning on line 12 in webviewer-redaction-ai/__mocks__/webviewer-redaction-ai.mock.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=ApryseSDK_webviewer-samples&issues=AZz6-sPo3yukrggiJdmU&open=AZz6-sPo3yukrggiJdmU&pullRequest=84

return process.env.NODE_ENV === 'mocking';
};

// Get mock response based on the requested type
export const getMockResponse = (responseType) => {
switch (responseType) {
case 'documentId':
return { documentId: MOCK_RESPONSES.documentId };
case 'analysis':
return { analysis: MOCK_RESPONSES.analysis };
default:
throw new Error(`Unknown mock response type: ${responseType}`);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// This file contains end-to-end tests for the AI PII redaction tool in WebViewer.
// It uses Playwright for testing the UI interactions and validating the functionality
// of the AI PII redaction tool.

import { test, expect } from '@playwright/test';

// Validating that the AI PII redaction tool button exists in the DOM.
test('Validate AI PII redaction tool button exists in DOM', async ({ page }) => {
// Go to the app page (adjust the URL if needed)
await page.goto('/client/index.html');

// Wait for 2 seconds to load the
// WebViewer with UI customization
await page.waitForTimeout(2000);

// Locate the AIPIIRedactionToolButton button in ModularHeader
let component = page.locator('button[data-element="AIPIIRedactionToolButton"]');
await expect(component).toHaveCount(1);
await expect(component).toBeVisible();
});

// Applying AI PII redaction to the document and validating the workflow of the tool.
test('Perform AI PII redaction', async ({ page }) => {
await page.goto('/client/index.html');

// Click the PII button to start identifying PII in the document
const pIIBtn = page.locator('button[data-element="AIPIIRedactionToolButton"]');
await pIIBtn.click();

// Wait for 7 seconds before finishing
// the test to capture the AI response
await page.waitForTimeout(7000);

// Click Redact All once AI marks are available.
const redactAllBtn = page.locator('button[data-element="redactAllMarkedButton"]');
await expect(redactAllBtn).toBeVisible();
await redactAllBtn.click();

// Click Redact All once AI marks are available.
const applyRedactionBtn = page.locator('button[data-element="WarningModalSignButton"]');
await expect(applyRedactionBtn).toBeVisible();
await applyRedactionBtn.click();

// Wait for 4 seconds before finishing
// the test to capture the AI response
await page.waitForTimeout(4000);
Comment thread
DavidEGutierrez marked this conversation as resolved.
Outdated
});
9 changes: 9 additions & 0 deletions webviewer-redaction-ai/__tests__/package.playwright.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Install Playwright and add test script
Comment thread
Mohammed-AbdulRahman-Apryse marked this conversation as resolved.
Outdated
{
"scripts": {
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "^1.43.1"
}
}
1 change: 1 addition & 0 deletions webviewer-redaction-ai/client/assets/ai-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions webviewer-redaction-ai/client/assets/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 120 additions & 0 deletions webviewer-redaction-ai/client/document/analyzer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { getMockResponse, isMockingModeEnabled } from '../../__mocks__/webviewer-redaction-ai.mock.js';

// Send the loaded document text to the server, to be
// analyzed for personal information identification (PII)
const sendTextToServer = async () => {
// *********************************************
// MOCKING MODE: Skip actual server call and
// return mock document id
if (isMockingModeEnabled())
return getMockResponse('documentId');
// *********************************************
Comment thread
Mohammed-AbdulRahman-Apryse marked this conversation as resolved.
Outdated

try {
const response = await fetch('/api/send-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
documentText: loadedDocument.text,
timestamp: new Date().toISOString()
}),
});

if (!response.ok)
throw new Error(`Server error: ${response.status} ${response.statusText}`);

return await response.json();
} catch (error) {
console.error('Error sending document text to server:', error);
throw error;
}
}

// Analyze the loaded document text for personal information identification (PII)
const analyzeDocument = async (documentId) => {
// *********************************************
// MOCKING MODE: Skip actual server call
if (isMockingModeEnabled())
return;
// *********************************************

try {
const response = await fetch('/api/analyze-pii', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
documentId: documentId,
timestamp: new Date().toISOString()
}),
});

if (!response.ok)
throw new Error(`Server error: ${response.status} ${response.statusText}`);

return await response.json();
} catch (error) {
console.error('Error analyzing document for PII:', error);
throw error;
}
}

// Receive analysis result from the server
const getAnalysisResult = async (documentId) => {
// *********************************************
// MOCKING MODE: Return mock analysis result
if (isMockingModeEnabled())
return getMockResponse('analysis');
// *********************************************

try {
const response = await fetch(`/api/get-results/${documentId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});

if (!response.ok)
throw new Error(`Server error: ${response.status} ${response.statusText}`);

return await response.json();
} catch (error) {
console.error('Error getting analysis result:', error);
throw error;
}
}

const analyzeDocumentForPII = async () => {
// Show WebViewer loading spinner
WebViewer.getInstance().UI.openElements('loadingModal');

// *********************************************
// MOCKING MODE: Keep spinner visible for
// 2 seconds
if (isMockingModeEnabled())
await new Promise((resolve) => setTimeout(resolve, 2000));
// *********************************************

try {
// Step 1: Send document text to server
const sendResult = await sendTextToServer();
const documentId = sendResult.documentId;

// Step 2: Analyze document for PII
await analyzeDocument(documentId);

// Step 3: Get analysis result from server
aiAnalysisResult = await getAnalysisResult(documentId);

Check failure on line 111 in webviewer-redaction-ai/client/document/analyzer.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add the "let", "const" or "var" keyword to this declaration of "aiAnalysisResult" to make it explicit.

See more on https://sonarcloud.io/project/issues?id=ApryseSDK_webviewer-samples&issues=AZz6-sOt3yukrggiJdmD&open=AZz6-sOt3yukrggiJdmD&pullRequest=84
} catch (error) {
console.error('Failed to analyze document:', error);
Comment thread
DavidEGutierrez marked this conversation as resolved.
}

// Hide WebViewer loading spinner
WebViewer.getInstance().UI.closeElements('loadingModal');
}

export { analyzeDocumentForPII };
42 changes: 42 additions & 0 deletions webviewer-redaction-ai/client/document/manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Manage the loaded document-related
// properties and operations.
// NOTE: The loaded document contents is cached in this object.
// Document contents edit requires saving then re-loading.
class DocumentManager {
#documentViewer;
#instance;
text;
#isValid;

constructor(documentViewer) {
this.#documentViewer = documentViewer;
this.#instance = null;
this.text = '';
this.#isValid = true;
}

async initialize() {
this.#instance = this.#documentViewer.getDocument();
if (!this.#isValid) {
console.error('Failed to initialize document manager.');
return;
}
const pageCount = this.#instance.getPageCount();
Comment thread
Mohammed-AbdulRahman-Apryse marked this conversation as resolved.
Outdated
await this.#instance.getDocumentCompletePromise().then(async () => {
// Load full document text
for (let pageIndex = 1; pageIndex <= pageCount; pageIndex++) {
try {
const pageText = await this.#instance.loadPageText(pageIndex);
this.text += `${pageText}\n`;
} catch (error) {
this.text += `[Error loading page content]\n`;
continue;
}

Check warning on line 34 in webviewer-redaction-ai/client/document/manager.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Handle this exception or don't catch it at all.

See more on https://sonarcloud.io/project/issues?id=ApryseSDK_webviewer-samples&issues=AZz6-sO23yukrggiJdmE&open=AZz6-sO23yukrggiJdmE&pullRequest=84
}
});
this.#isValid = this.#instance &&
this.text.length > 0;
}
}

export default DocumentManager;
9 changes: 9 additions & 0 deletions webviewer-redaction-ai/client/globals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Globals for the app
let loadedDocument = null;
let aiAnalysisResult = null;

const files = [
'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/sales-invoice-with-credit-cards.pdf',
'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/employee-360-review.pdf',
'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/section-508.pdf',
];
Comment thread
Mohammed-AbdulRahman-Apryse marked this conversation as resolved.
Outdated
Loading
Loading