-
Notifications
You must be signed in to change notification settings - Fork 18
webviewer-redaction-ai sample #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
4508f8a
80ad583
4191423
06d503f
0777668
aedbbd3
700ed15
c51acb5
6f0c726
cd7248a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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 |
| 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). |
| 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. |
| 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
|
||
| return window.MODE_ENV === 'mocking'; | ||
|
Check warning on line 12 in webviewer-redaction-ai/__mocks__/webviewer-redaction-ai.mock.js
|
||
|
|
||
| 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); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| // Install Playwright and add test script | ||
|
Mohammed-AbdulRahman-Apryse marked this conversation as resolved.
Outdated
|
||
| { | ||
| "scripts": { | ||
| "test": "playwright test" | ||
| }, | ||
| "devDependencies": { | ||
| "@playwright/test": "^1.43.1" | ||
| } | ||
| } | ||
| 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'); | ||
| // ********************************************* | ||
|
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
|
||
| } catch (error) { | ||
| console.error('Failed to analyze document:', error); | ||
|
DavidEGutierrez marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // Hide WebViewer loading spinner | ||
| WebViewer.getInstance().UI.closeElements('loadingModal'); | ||
| } | ||
|
|
||
| export { analyzeDocumentForPII }; | ||
| 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(); | ||
|
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
|
||
| } | ||
| }); | ||
| this.#isValid = this.#instance && | ||
| this.text.length > 0; | ||
| } | ||
| } | ||
|
|
||
| export default DocumentManager; | ||
| 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', | ||
| ]; | ||
|
Mohammed-AbdulRahman-Apryse marked this conversation as resolved.
Outdated
|
||
Uh oh!
There was an error while loading. Please reload this page.