diff --git a/README.md b/README.md
index 6e2d4a7..cb4752b 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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
+
diff --git a/lerna.json b/lerna.json
index 494ce83..f31ce43 100644
--- a/lerna.json
+++ b/lerna.json
@@ -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",
diff --git a/webviewer-redaction-ai/.env.example b/webviewer-redaction-ai/.env.example
new file mode 100644
index 0000000..8be0bdb
--- /dev/null
+++ b/webviewer-redaction-ai/.env.example
@@ -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
\ No newline at end of file
diff --git a/webviewer-redaction-ai/.gitignore b/webviewer-redaction-ai/.gitignore
new file mode 100644
index 0000000..8bc4ae2
--- /dev/null
+++ b/webviewer-redaction-ai/.gitignore
@@ -0,0 +1,7 @@
+# Misc
+.DS_Store
+node_modules
+
+# WebViewer
+client/lib
+client/license-key.js
\ No newline at end of file
diff --git a/webviewer-redaction-ai/LICENSE b/webviewer-redaction-ai/LICENSE
new file mode 100644
index 0000000..5bde132
--- /dev/null
+++ b/webviewer-redaction-ai/LICENSE
@@ -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).
\ No newline at end of file
diff --git a/webviewer-redaction-ai/README.md b/webviewer-redaction-ai/README.md
new file mode 100644
index 0000000..42209a7
--- /dev/null
+++ b/webviewer-redaction-ai/README.md
@@ -0,0 +1,49 @@
+# WebViewer - Redaction AI sample
+
+Add an AI-powered assistant to WebViewer, detect Personally Identifiable Information (PII) 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/)
+
+
+
+## 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.
\ No newline at end of file
diff --git a/webviewer-redaction-ai/__mocks__/webviewer-redaction-ai.mock.js b/webviewer-redaction-ai/__mocks__/webviewer-redaction-ai.mock.js
new file mode 100644
index 0000000..17b4c15
--- /dev/null
+++ b/webviewer-redaction-ai/__mocks__/webviewer-redaction-ai.mock.js
@@ -0,0 +1,72 @@
+// This file is used to mock the server responses for document analysis in testing scenarios.
+
+export const MOCK_DATA = {
+ 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',
+ 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'
+};
+
+// Utility function to safely parse JSON
+// returning an empty object on failure.
+const safeJsonParse = (value) => {
+ try {
+ return JSON.parse(value || '{}');
+ } catch {
+ return {};
+ }
+};
+
+// Helper function to create a JSON
+// response for route fulfillment.
+const jsonResponse = (payload) => ({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify(payload)
+});
+
+// Registers API route mocks for the AI PII redaction tool tests.
+export const registerApiRouteMocks = async (page) => {
+ const calls = {
+ sendText: 0,
+ analyzePII: 0,
+ getResults: 0,
+ sendTextPayload: null,
+ analyzePIIPayload: null
+ };
+
+ // Mocking the send-text endpoint to capture document text and return a success response.
+ await page.route('**/api/send-text', async (route) => {
+ calls.sendText += 1;
+ calls.sendTextPayload = safeJsonParse(route.request().postData());
+
+ await route.fulfill(jsonResponse({
+ success: true,
+ message: 'Document text received successfully',
+ textLength: calls.sendTextPayload.documentText?.length || 0,
+ pageCount: calls.sendTextPayload.documentText?.pageCount || 0
+ }));
+ });
+
+ // Mocking the analyze-pii endpoint to capture analysis requests and return a success response.
+ await page.route('**/api/analyze-pii', async (route) => {
+ calls.analyzePII += 1;
+ calls.analyzePIIPayload = safeJsonParse(route.request().postData());
+
+ await route.fulfill(jsonResponse({
+ success: true,
+ message: 'PII analysis completed'
+ }));
+ });
+
+ // Mocking the get-results endpoint to return the analysis results.
+ await page.route('**/api/get-results', async (route) => {
+ calls.getResults += 1;
+ await route.fulfill(jsonResponse({
+ success: true,
+ message: 'Document analyzed successfully with AI',
+ aiProcessing: true,
+ analysis: MOCK_DATA.analysisText
+ }));
+ });
+
+ return calls;
+};
\ No newline at end of file
diff --git a/webviewer-redaction-ai/__tests__/e2e/webviewer-redaction-ai.spec.js b/webviewer-redaction-ai/__tests__/e2e/webviewer-redaction-ai.spec.js
new file mode 100644
index 0000000..2cbeb25
--- /dev/null
+++ b/webviewer-redaction-ai/__tests__/e2e/webviewer-redaction-ai.spec.js
@@ -0,0 +1,174 @@
+// 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';
+import { MOCK_DATA, registerApiRouteMocks } from '../../__mocks__/webviewer-redaction-ai.mock.js';
+
+let mockCalls = null;
+
+// Before each test, we set up the API route mocks to intercept and
+// simulate backend responses for the AI PII redaction workflow.
+// This allows us to test the UI interactions and logic without
+// relying on actual backend services.
+test.beforeEach(async ({ page }) => {
+ mockCalls = await registerApiRouteMocks(page);
+});
+
+const normalizeMultilineText = (value) =>
+ (value || '')
+ .split('\n')
+ .map((line) => line.trimEnd())
+ .filter((line) => line !== '')
+ .join('\n');
+
+// Validating that the AI PII redaction tool button exists in the DOM.
+test('PII redaction tool button visibility', async ({ page }) => {
+ // Go to the app page (adjust the URL if needed)
+ await page.goto('/client/index.html');
+
+ // Click the Redact tab to activate the Redact toolbar group.
+ await page.locator('button[data-element="toolbarGroup-Redact"]').click();
+
+ // 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 with mocked endpoint responses.
+test('Perform AI PII redaction for document text size and page count within limits', async ({ page }) => {
+ await page.goto('/client/index.html');
+
+ // Click the Redact tab to activate the Redact toolbar group.
+ await page.locator('button[data-element="toolbarGroup-Redact"]').click();
+
+ // Ensure full document text is loaded before triggering analysis requests.
+ await expect.poll(
+ () => page.evaluate(() => globalThis.loadedDocument?.text || '')
+ ).toContain('Peady, Eff, & Wright Exporting');
+
+ // Ensure full document text length/size is within limits before triggering analysis requests.
+ await expect.poll(
+ () => page.evaluate(() => globalThis.loadedDocument?.text.length || 0)
+ ).toBeLessThanOrEqual(30000);
+
+ // Ensure page count is within limits before triggering analysis requests.
+ await expect.poll(
+ () => page.evaluate(() => globalThis.loadedDocument?.pageCount || 0)
+ ).toBeLessThanOrEqual(20);
+
+ // Trigger analyzeDocumentForPII + applyRedactions through the UI button.
+ const pIIBtn = page.locator('button[data-element="AIPIIRedactionToolButton"]');
+ await pIIBtn.click();
+
+ // Verify mocked endpoint workflow for send -> analyze -> get result.
+ await expect.poll(() => mockCalls.sendText).toBe(1);
+ await expect.poll(() => mockCalls.analyzePII).toBe(1);
+ await expect.poll(() => mockCalls.getResults).toBe(1);
+ await expect.poll(() => normalizeMultilineText(mockCalls.sendTextPayload?.documentText)).toBe(
+ normalizeMultilineText(MOCK_DATA.documentText)
+ );
+
+ // Complete the redaction flow and assert it closes cleanly.
+ const redactAllBtn = page.locator('button[data-element="redactAllMarkedButton"]');
+ await expect(redactAllBtn).toBeVisible();
+ await redactAllBtn.click();
+
+ // Confirm the apply redaction modal and ensure it disappears after confirming.
+ const applyRedactionBtn = page.locator('button[data-element="WarningModalSignButton"]');
+ await expect(applyRedactionBtn).toBeVisible();
+ await applyRedactionBtn.click();
+ await expect(applyRedactionBtn).toBeHidden();
+
+ // Ensure mocked analysis text is the value used by the UI workflow.
+ await expect.poll(() => page.evaluate(() => globalThis.aiAnalysisResult?.analysis)).toBe(MOCK_DATA.analysisText);
+});
+
+// Expect an alert when analysis completes but /api/get-results returns no PII findings.
+test('Expect an alert when loaded document has no identifiable PII', async ({ page }) => {
+ await page.goto('/client/index.html');
+
+ // Click the Redact tab to activate the Redact toolbar group.
+ await page.locator('button[data-element="toolbarGroup-Redact"]').click();
+
+ // Wait until document wrapper is initialized before replacing the loaded text.
+ await expect.poll(() => page.evaluate(() => Boolean(globalThis.loadedDocument))).toBe(true);
+
+ const noPIIText = 'Quarterly operations summary for warehouse inventory and packaging materials.';
+ await page.evaluate((text) => {
+ globalThis.loadedDocument.text = text;
+ globalThis.loadedDocument.pageCount = 1;
+ }, noPIIText);
+
+ let getResultsCalls = 0;
+ await page.unroute('**/api/get-results');
+ await page.route('**/api/get-results', async (route) => {
+ getResultsCalls += 1;
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ success: false,
+ error: 'No analysis results found. Please analyze the document first.'
+ })
+ });
+ });
+
+ // Trigger analyzeDocumentForPII through the UI button and verify the alert content.
+ const pIIBtn = page.locator('button[data-element="AIPIIRedactionToolButton"]');
+ const alertPromise = page.waitForEvent('dialog');
+ await pIIBtn.click();
+ const alertDialog = await alertPromise;
+ expect(alertDialog.message()).toBe('No PII result found.');
+ await alertDialog.accept();
+
+ // Ensure the no-PII document text was submitted and the flow reached /api/get-results.
+ await expect.poll(() => mockCalls.sendText).toBe(1);
+ await expect.poll(() => mockCalls.analyzePII).toBe(1);
+ await expect.poll(() => getResultsCalls).toBe(1);
+ await expect.poll(() => normalizeMultilineText(mockCalls.sendTextPayload?.documentText)).toBe(noPIIText);
+});
+
+// Expect an alert when document text/pages values exceed limits.
+test('Expect an alert when document text exceeds 30000 characters with page count above 20', async ({ page }) => {
+ await page.goto('/client/index.html');
+
+ // Click the Redact tab to activate the Redact toolbar group.
+ await page.locator('button[data-element="toolbarGroup-Redact"]').click();
+
+ // Wait until document wrapper is initialized before mutating fields.
+ await expect.poll(() => page.evaluate(() => Boolean(globalThis.loadedDocument))).toBe(true);
+
+ await page.evaluate(() => {
+ globalThis.loadedDocument.text = 'A'.repeat(30001);
+ globalThis.loadedDocument.pageCount = 21;
+ });
+
+ let sendTextCalls = 0;
+ await page.unroute('**/api/send-text');
+ await page.route('**/api/send-text', async (route) => {
+ sendTextCalls += 1;
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ success: false,
+ error: 'Document text size exceeds 30000 characters limit.'
+ })
+ });
+ });
+
+ // Trigger analyzeDocumentForPII through the UI button and verify the alert content.
+ const pIIBtn = page.locator('button[data-element="AIPIIRedactionToolButton"]');
+ const alertPromise = page.waitForEvent('dialog');
+ await pIIBtn.click();
+ const alertDialog = await alertPromise;
+ expect(alertDialog.message()).toBe('Document text size exceeds 30000 characters limit.');
+ await alertDialog.accept();
+
+ // Ensure request flow stops at /api/send-text when request validation fails.
+ await expect.poll(() => sendTextCalls).toBe(1);
+ await expect.poll(() => mockCalls.analyzePII).toBe(0);
+ await expect.poll(() => mockCalls.getResults).toBe(0);
+});
\ No newline at end of file
diff --git a/webviewer-redaction-ai/__tests__/package.playwright.json b/webviewer-redaction-ai/__tests__/package.playwright.json
new file mode 100644
index 0000000..a138c2e
--- /dev/null
+++ b/webviewer-redaction-ai/__tests__/package.playwright.json
@@ -0,0 +1,8 @@
+{
+ "scripts": {
+ "test": "playwright test"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.43.1"
+ }
+}
diff --git a/webviewer-redaction-ai/client/assets/ai-icon.svg b/webviewer-redaction-ai/client/assets/ai-icon.svg
new file mode 100644
index 0000000..cba93bd
--- /dev/null
+++ b/webviewer-redaction-ai/client/assets/ai-icon.svg
@@ -0,0 +1 @@
+
diff --git a/webviewer-redaction-ai/client/assets/favicon.svg b/webviewer-redaction-ai/client/assets/favicon.svg
new file mode 100644
index 0000000..6586460
--- /dev/null
+++ b/webviewer-redaction-ai/client/assets/favicon.svg
@@ -0,0 +1,13 @@
+
diff --git a/webviewer-redaction-ai/client/document/analyzer.js b/webviewer-redaction-ai/client/document/analyzer.js
new file mode 100644
index 0000000..06585d5
--- /dev/null
+++ b/webviewer-redaction-ai/client/document/analyzer.js
@@ -0,0 +1,115 @@
+// Send the loaded document text to the server, to be
+// analyzed for personal information identification (PII)
+const sendTextToServer = async () => {
+ try {
+ const response = await fetch('/api/send-text', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ documentText: globalThis.loadedDocument?.text ?? '',
+ pageCount: globalThis.loadedDocument?.pageCount ?? 0
+ }),
+ });
+
+ 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 () => {
+ try {
+ const response = await fetch('/api/analyze-pii', {
+ method: 'POST',
+ 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 analyzing document for PII:', error);
+ throw error;
+ }
+}
+
+// Receive analysis result from the server
+const getAnalysisResult = async () => {
+ try {
+ const response = await fetch('/api/get-results', {
+ 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 () => {
+ try {
+ // Clear any existing AI-generated redactions before applying new ones
+ clearAIRedactions();
+
+ // Show WebViewer loading spinner
+ WebViewer.getInstance().UI.openElements('loadingModal');
+
+ // Step 1: Send document text to server
+ let response = await sendTextToServer();
+ if (!response.success) {
+ alert(response.error);
+ return false;
+ }
+
+ // Step 2: Analyze document for PII
+ response = await analyzeDocument();
+ if (!response.success)
+ return false;
+
+ // Step 3: Get analysis result from server
+ globalThis.aiAnalysisResult = await getAnalysisResult();
+ if (!globalThis.aiAnalysisResult.success) {
+ alert('No PII result found.');
+ return false;
+ }
+
+ return true;
+ } catch (error) {
+ console.error('Failed to analyze document:', error);
+ return false;
+ }
+ finally {
+ // Hide WebViewer loading spinner
+ WebViewer.getInstance().UI.closeElements('loadingModal');
+ }
+}
+
+// Function to clear all AI-generated redaction annotations
+const clearAIRedactions = () => {
+ const { annotationManager, Annotations } = WebViewer.getInstance().Core;
+ // Get all redaction annotations with 'AI Redaction' Author
+ const redactionList = annotationManager.getAnnotationsList().filter(
+ annot => annot instanceof Annotations.RedactionAnnotation && annot.Author === 'AI Redaction'
+ );
+
+ // Delete each matching redaction annotation
+ redactionList.forEach(redaction => {
+ annotationManager.deleteAnnotation(redaction, true);
+ });
+}
+
+export { analyzeDocumentForPII };
\ No newline at end of file
diff --git a/webviewer-redaction-ai/client/document/manager.js b/webviewer-redaction-ai/client/document/manager.js
new file mode 100644
index 0000000..75e72a6
--- /dev/null
+++ b/webviewer-redaction-ai/client/document/manager.js
@@ -0,0 +1,45 @@
+// 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;
+ pageCount;
+
+ constructor(documentViewer) {
+ this.#documentViewer = documentViewer;
+ this.#instance = null;
+ this.text = '';
+ this.#isValid = true;
+ this.pageCount = 0;
+ }
+
+ async initialize() {
+ this.#instance = this.#documentViewer.getDocument();
+ this.#isValid = !!this.#instance;
+ if (!this.#isValid) {
+ console.error('Failed to initialize document manager.');
+ return;
+ }
+ this.pageCount = this.#instance.getPageCount();
+ await this.#instance.getDocumentCompletePromise().then(async () => {
+ // Load full document text
+ for (let pageIndex = 1; pageIndex <= this.pageCount; pageIndex++) {
+ try {
+ const pageText = await this.#instance.loadPageText(pageIndex);
+ this.text += `${pageText}\n`;
+ } catch (error) {
+ this.text += `[Error loading page content: ${error.message}]\n`;
+ continue;
+ }
+ }
+ });
+ this.#isValid = this.#instance &&
+ this.text.length > 0;
+ }
+}
+
+export default DocumentManager;
\ No newline at end of file
diff --git a/webviewer-redaction-ai/client/globals.js b/webviewer-redaction-ai/client/globals.js
new file mode 100644
index 0000000..64f29d9
--- /dev/null
+++ b/webviewer-redaction-ai/client/globals.js
@@ -0,0 +1,13 @@
+// Globals for the app
+let loadedDocument = null;
+globalThis.loadedDocument = loadedDocument;
+let aiAnalysisResult = null;
+globalThis.aiAnalysisResult = aiAnalysisResult;
+
+
+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',
+];
+globalThis.files = files;
\ No newline at end of file
diff --git a/webviewer-redaction-ai/client/index.html b/webviewer-redaction-ai/client/index.html
new file mode 100644
index 0000000..db2ee30
--- /dev/null
+++ b/webviewer-redaction-ai/client/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+ WebViewer Redaction AI
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webviewer-redaction-ai/client/index.js b/webviewer-redaction-ai/client/index.js
new file mode 100644
index 0000000..6afbfe4
--- /dev/null
+++ b/webviewer-redaction-ai/client/index.js
@@ -0,0 +1,45 @@
+import DocumentManager from './document/manager.js';
+import functionMap from './ui/functionMap.js';
+const customUIFile = './ui/custom.json';
+
+const instance = await WebViewer({
+ path: 'lib',
+ initialDoc: globalThis.files[0],
+ fullAPI: true,
+ loadAsPDF: true,
+ enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
+ enableRedaction: true,
+ licenseKey: 'YOUR_LICENSE_KEY',
+}, document.getElementById('viewer'));
+
+const { documentViewer } = instance.Core;
+const { UI } = instance;
+
+// Import modular components configuration from JSON file
+try {
+ const response = await fetch(customUIFile);
+ if (!response.ok)
+ throw new Error(`Failed to import modular components configuration: ${response.statusText}`);
+
+ let customUIConfig = JSON.stringify(await response.json());
+ WebViewer.getInstance().UI.importModularComponents(JSON.parse(customUIConfig), functionMap);
+} catch (error) {
+ throw new Error(`Failed to import modular components configuration: ${error.message}`);
+}
+
+documentViewer.addEventListener('documentLoaded', async () => {
+ // Switch to the Redact toolbar group
+ UI.setToolbarGroup('toolbarGroup-Redact');
+
+ // Set layout to Facing Continuous and fit mode
+ // to Fit Page for better redaction experience
+ UI.setLayoutMode(UI.LayoutMode.FacingContinuous);
+ UI.setFitMode(UI.FitMode.FitPage);
+
+ // Load document manager
+ globalThis.loadedDocument = new DocumentManager(documentViewer);
+ await globalThis.loadedDocument.initialize().then(() => {
+ }).catch(error => {
+ console.error('Failed to initialize document manager:', error);
+ });
+});
\ No newline at end of file
diff --git a/webviewer-redaction-ai/client/redaction.js b/webviewer-redaction-ai/client/redaction.js
new file mode 100644
index 0000000..7b4aa36
--- /dev/null
+++ b/webviewer-redaction-ai/client/redaction.js
@@ -0,0 +1,73 @@
+// Function to create redaction annotations from AI analysis results
+const applyRedactions = async () => {
+ const { documentViewer, Search, annotationManager, Annotations } = WebViewer.getInstance().Core;
+
+ // Ensure the user has permissions to create/edit/delete annotations
+ annotationManager.promoteUserToAdmin();
+
+ // Parse the analysis to extract PII entities
+ let piiEntities = [];
+ try {
+ // Extract entities from the analysis (assuming format from AI response)
+ const lines = globalThis.aiAnalysisResult.analysis.split('\n');
+ lines.forEach(line => {
+ if (line.trim() !== '')
+ piiEntities.push(line.trim());
+ });
+ } catch (error) {
+ console.error('Error parsing analysis result:', error);
+ return;
+ }
+
+ if (piiEntities.length === 0)
+ return;
+
+ // Configure the search modes
+ const modes = [
+ Search.Mode.PAGE_STOP,
+ Search.Mode.HIGHLIGHT,
+ Search.Mode.CASE_SENSITIVE,
+ Search.Mode.WHOLE_WORD
+ ];
+ const searchMode = modes.reduce((combinedModes, mode) => combinedModes | mode, 0);
+
+ // Search for PII entities then create redactions
+ let searchOptions = null;
+ for (const searchText of piiEntities) {
+ await new Promise((resolve) => {
+ searchOptions = {
+ // search the entire document
+ fullSearch: true,
+ // called when search is complete
+ onDocumentEnd: () => {
+ resolve();
+ },
+ // called when a search result is found
+ onResult: (result) => {
+ if (result.resultCode === Search.ResultCode.FOUND) {
+ // Get the page number and the quads for the search result
+ const { pageNum, quads } = result;
+
+ // Create a new redaction annotation using the quads and page number
+ const redactAnnot = new Annotations.RedactionAnnotation({
+ PageNumber: pageNum,
+ Quads: quads.map((quad) => quad.getPoints())
+ });
+
+ redactAnnot.Author = "AI Redaction";
+ redactAnnot.setContents("Redacted PII");
+ redactAnnot.setCustomData('trn-annot-preview', documentViewer.getSelectedText(redactAnnot.PageNumber));
+
+ // Apply and redraw the redaction annotation
+ annotationManager.addAnnotations([redactAnnot]);
+ annotationManager.drawAnnotationsFromList([redactAnnot]);
+ }
+ },
+ };
+
+ documentViewer.textSearchInit(searchText, searchMode, searchOptions);
+ });
+ }
+};
+
+export { applyRedactions };
\ No newline at end of file
diff --git a/webviewer-redaction-ai/client/ui/custom.json b/webviewer-redaction-ai/client/ui/custom.json
new file mode 100644
index 0000000..40304af
--- /dev/null
+++ b/webviewer-redaction-ai/client/ui/custom.json
@@ -0,0 +1,1136 @@
+{
+ "modularComponents": {
+ "filePickerButton": {
+ "dataElement": "filePickerButton",
+ "title": "action.openFile",
+ "label": "action.openFile",
+ "type": "presetButton",
+ "buttonType": "filePickerButton"
+ },
+ "downloadButton": {
+ "dataElement": "downloadButton",
+ "title": "action.download",
+ "label": "action.download",
+ "type": "presetButton",
+ "buttonType": "downloadButton",
+ "disabled": true
+ },
+ "saveAsButton": {
+ "dataElement": "saveAsButton",
+ "title": "saveModal.saveAs",
+ "isActive": false,
+ "label": "saveModal.saveAs",
+ "type": "presetButton",
+ "buttonType": "saveAsButton",
+ "disabled": true
+ },
+ "printButton": {
+ "dataElement": "printButton",
+ "title": "action.print",
+ "isActive": false,
+ "label": "action.print",
+ "type": "presetButton",
+ "buttonType": "printButton",
+ "disabled": true
+ },
+ "createPortfolioButton": {
+ "dataElement": "createPortfolioButton",
+ "title": "portfolio.createPDFPortfolio",
+ "isActive": false,
+ "label": "portfolio.createPDFPortfolio",
+ "type": "presetButton",
+ "buttonType": "createPortfolioButton",
+ "disabled": true
+ },
+ "settingsButton": {
+ "dataElement": "settingsButton",
+ "title": "option.settings.settings",
+ "isActive": false,
+ "label": "option.settings.settings",
+ "type": "presetButton",
+ "buttonType": "settingsButton"
+ },
+ "divider-0.1": {
+ "dataElement": "divider-0.1",
+ "disabled": false,
+ "type": "divider"
+ },
+ "leftPanelButton": {
+ "dataElement": "leftPanelButton",
+ "title": "component.leftPanel",
+ "disabled": true,
+ "type": "toggleButton",
+ "img": "icon-header-sidebar-line",
+ "toggleElement": "tabPanel"
+ },
+ "view-controls": {
+ "dataElement": "view-controls",
+ "title": "component.viewControls",
+ "type": "viewControls",
+ "icon": "icon-header-page-manipulation-line"
+ },
+ "divider-0.3": {
+ "dataElement": "divider-0.3",
+ "disabled": false,
+ "type": "divider"
+ },
+ "zoom-container": {
+ "dataElement": "zoom-container",
+ "type": "zoom"
+ },
+ "divider-0.2": {
+ "dataElement": "divider-0.2",
+ "disabled": false,
+ "type": "divider"
+ },
+ "panToolButton": {
+ "dataElement": "panToolButton",
+ "disabled": true,
+ "type": "toolButton",
+ "toolName": "Pan"
+ },
+ "annotationEditToolButton": {
+ "dataElement": "annotationEditToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationEdit"
+ },
+ "menuButton": {
+ "dataElement": "menuButton",
+ "title": "component.menuOverlay",
+ "type": "toggleButton",
+ "img": "ic-hamburger-menu",
+ "toggleElement": "MainMenuFlyout"
+ },
+ "toolbarGroup-View": {
+ "dataElement": "toolbarGroup-View",
+ "title": "View",
+ "disabled": false,
+ "type": "ribbonItem",
+ "label": "View",
+ "groupedItems": [],
+ "toolbarGroup": "toolbarGroup-View"
+ },
+ "toolbarGroup-Annotate": {
+ "dataElement": "toolbarGroup-Annotate",
+ "title": "Annotate",
+ "disabled": true,
+ "type": "ribbonItem",
+ "label": "Annotate",
+ "groupedItems": ["annotateGroupedItems"],
+ "toolbarGroup": "toolbarGroup-Annotate"
+ },
+ "toolbarGroup-Shapes": {
+ "dataElement": "toolbarGroup-Shapes",
+ "title": "Shapes",
+ "disabled": true,
+ "type": "ribbonItem",
+ "label": "Shapes",
+ "groupedItems": ["shapesGroupedItems"],
+ "toolbarGroup": "toolbarGroup-Shapes"
+ },
+ "toolbarGroup-Insert": {
+ "dataElement": "toolbarGroup-Insert",
+ "title": "Insert",
+ "disabled": true,
+ "type": "ribbonItem",
+ "label": "Insert",
+ "groupedItems": ["insertGroupedItems"],
+ "toolbarGroup": "toolbarGroup-Insert"
+ },
+ "toolbarGroup-Measure": {
+ "dataElement": "toolbarGroup-Measure",
+ "title": "Measure",
+ "disabled": true,
+ "type": "ribbonItem",
+ "label": "Measure",
+ "groupedItems": ["measureGroupedItems"],
+ "toolbarGroup": "toolbarGroup-Measure"
+ },
+ "toolbarGroup-Redact": {
+ "dataElement": "toolbarGroup-Redact",
+ "title": "Redact",
+ "disabled": false,
+ "type": "ribbonItem",
+ "label": "Redact",
+ "groupedItems": ["redactionGroupedItems"],
+ "toolbarGroup": "toolbarGroup-Redact"
+ },
+ "toolbarGroup-Edit": {
+ "dataElement": "toolbarGroup-Edit",
+ "title": "Edit",
+ "disabled": true,
+ "type": "ribbonItem",
+ "label": "Edit",
+ "groupedItems": ["editGroupedItems"],
+ "toolbarGroup": "toolbarGroup-Edit"
+ },
+ "toolbarGroup-EditText": {
+ "dataElement": "toolbarGroup-EditText",
+ "title": "Content Edit",
+ "disabled": true,
+ "type": "ribbonItem",
+ "label": "Content Edit",
+ "groupedItems": ["contentEditGroupedItems"],
+ "toolbarGroup": "toolbarGroup-EditText"
+ },
+ "toolbarGroup-FillAndSign": {
+ "dataElement": "toolbarGroup-FillAndSign",
+ "title": "Fill and Sign",
+ "disabled": true,
+ "type": "ribbonItem",
+ "label": "Fill and Sign",
+ "groupedItems": ["fillAndSignGroupedItems"],
+ "toolbarGroup": "toolbarGroup-FillAndSign"
+ },
+ "toolbarGroup-Forms": {
+ "dataElement": "toolbarGroup-Forms",
+ "title": "Forms",
+ "disabled": true,
+ "type": "ribbonItem",
+ "label": "Forms",
+ "groupedItems": ["formsGroupedItems"],
+ "toolbarGroup": "toolbarGroup-Forms"
+ },
+ "highlightToolButton": {
+ "dataElement": "highlightToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateTextHighlight"
+ },
+ "underlineToolButton": {
+ "dataElement": "underlineToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateTextUnderline"
+ },
+ "strikeoutToolButton": {
+ "dataElement": "strikeoutToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateTextStrikeout"
+ },
+ "squigglyToolButton": {
+ "dataElement": "squigglyToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateTextSquiggly"
+ },
+ "freeTextToolButton": {
+ "dataElement": "freeTextToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateFreeText"
+ },
+ "markInsertTextToolButton": {
+ "dataElement": "markInsertTextToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateMarkInsertText"
+ },
+ "markReplaceTextToolButton": {
+ "dataElement": "markReplaceTextToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateMarkReplaceText"
+ },
+ "freeHandToolButton": {
+ "dataElement": "freeHandToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateFreeHand"
+ },
+ "freeHandHighlightToolButton": {
+ "dataElement": "freeHandHighlightToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateFreeHandHighlight"
+ },
+ "stickyToolButton": {
+ "dataElement": "stickyToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateSticky"
+ },
+ "calloutToolButton": {
+ "dataElement": "calloutToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateCallout"
+ },
+ "divider-0.4": {
+ "dataElement": "divider-0.4",
+ "type": "divider"
+ },
+ "stylePanelToggle": {
+ "dataElement": "stylePanelToggle",
+ "title": "action.style",
+ "type": "toggleButton",
+ "img": "icon-style-panel-toggle",
+ "toggleElement": "stylePanel"
+ },
+ "indexPanelListToggle": {
+ "dataElement": "indexPanelListToggle",
+ "title": "component.indexPanel",
+ "type": "toggleButton",
+ "img": "icon-index-panel-list",
+ "toggleElement": "indexPanel"
+ },
+ "divider-0.5": {
+ "dataElement": "divider-0.5",
+ "type": "divider"
+ },
+ "undoButton": {
+ "dataElement": "undoButton",
+ "type": "presetButton",
+ "buttonType": "undoButton"
+ },
+ "redoButton": {
+ "dataElement": "redoButton",
+ "type": "presetButton",
+ "buttonType": "redoButton"
+ },
+ "toggleAccessibilityModeButton": {
+ "dataElement": "toggleAccessibilityModePresetButton",
+ "type": "presetButton",
+ "buttonType": "toggleAccessibilityModeButton"
+ },
+ "eraserToolButton": {
+ "dataElement": "eraserToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationEraserTool"
+ },
+ "defaultAnnotationUtilities": {
+ "dataElement": "defaultAnnotationUtilities",
+ "items": ["divider-0.5", "undoButton", "redoButton", "eraserToolButton"],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "annotateToolsGroupedItems": {
+ "dataElement": "annotateToolsGroupedItems",
+ "items": [
+ "highlightToolButton",
+ "underlineToolButton",
+ "strikeoutToolButton",
+ "squigglyToolButton",
+ "freeHandToolButton",
+ "freeHandHighlightToolButton",
+ "freeTextToolButton",
+ "markInsertTextToolButton",
+ "markReplaceTextToolButton",
+ "stickyToolButton",
+ "calloutToolButton"
+ ],
+ "type": "groupedItems",
+ "justifyContent": "center",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "annotateGroupedItems": {
+ "dataElement": "annotateGroupedItems",
+ "items": [
+ "annotateToolsGroupedItems",
+ "divider-0.4",
+ "stylePanelToggle",
+ "defaultAnnotationUtilities"
+ ],
+ "type": "groupedItems",
+ "justifyContent": "center",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "rectangleToolButton": {
+ "dataElement": "rectangleToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateRectangle"
+ },
+ "ellipseToolButton": {
+ "dataElement": "ellipseToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateEllipse"
+ },
+ "arcToolButton": {
+ "dataElement": "arcToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateArc"
+ },
+ "polygonToolButton": {
+ "dataElement": "polygonToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreatePolygon"
+ },
+ "cloudToolButton": {
+ "dataElement": "cloudToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreatePolygonCloud"
+ },
+ "lineToolButton": {
+ "dataElement": "lineToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateLine"
+ },
+ "polylineToolButton": {
+ "dataElement": "polylineToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreatePolyline"
+ },
+ "arrowToolButton": {
+ "dataElement": "arrowToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateArrow"
+ },
+ "shapesToolsGroupedItems": {
+ "dataElement": "shapesToolsGroupedItems",
+ "items": [
+ "rectangleToolButton",
+ "ellipseToolButton",
+ "arcToolButton",
+ "polygonToolButton",
+ "cloudToolButton",
+ "lineToolButton",
+ "polylineToolButton",
+ "arrowToolButton"
+ ],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "shapesGroupedItems": {
+ "dataElement": "shapesGroupedItems",
+ "items": [
+ "shapesToolsGroupedItems",
+ "divider-0.4",
+ "stylePanelToggle",
+ "defaultAnnotationUtilities"
+ ],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "rubberStampToolButton": {
+ "dataElement": "rubberStampToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateRubberStamp"
+ },
+ "signatureCreateToolButton": {
+ "dataElement": "signatureCreateToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateSignature"
+ },
+ "fileAttachmentButton": {
+ "dataElement": "fileAttachmentButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateFileAttachment"
+ },
+ "stampToolButton": {
+ "dataElement": "stampToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateStamp"
+ },
+ "insertToolsGroupedItems": {
+ "dataElement": "insertToolsGroupedItems",
+ "items": [
+ "rubberStampToolButton",
+ "signatureCreateToolButton",
+ "fileAttachmentButton",
+ "stampToolButton"
+ ],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "insertGroupedItems": {
+ "dataElement": "insertGroupedItems",
+ "items": [
+ "insertToolsGroupedItems",
+ "divider-0.4",
+ "stylePanelToggle",
+ "defaultAnnotationUtilities"
+ ],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "AIPIIRedactionToolButton": {
+ "dataElement": "AIPIIRedactionToolButton",
+ "type": "customButton",
+ "toolName": "AIPIIRedaction",
+ "title": "AI - Personally Identifiable Information (PII)",
+ "img": "assets/ai-icon.svg",
+ "onClick": "AIPIIRedactionClick"
+ },
+ "redactionToolButton": {
+ "dataElement": "redactionToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateRedaction"
+ },
+ "pageRedactionToggleButton": {
+ "dataElement": "pageRedactionToggleButton",
+ "title": "action.redactPages",
+ "type": "toggleButton",
+ "img": "icon-tool-page-redact",
+ "toggleElement": "pageRedactionModal"
+ },
+ "redactionPanelToggle": {
+ "dataElement": "redactionPanelToggle",
+ "type": "toggleButton",
+ "img": "icon-redact-panel",
+ "toggleElement": "redactionPanel",
+ "title": "component.redactionPanel"
+ },
+ "redactionGroupedItems": {
+ "dataElement": "redactionGroupedItems",
+ "items": [
+ "AIPIIRedactionToolButton",
+ "divider-0.4",
+ "redactionToolButton",
+ "pageRedactionToggleButton",
+ "redactionPanelToggle",
+ "divider-0.4",
+ "stylePanelToggle",
+ "defaultAnnotationUtilities"
+ ],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "distanceMeasurementToolButton": {
+ "dataElement": "distanceMeasurementToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateDistanceMeasurement"
+ },
+ "arcMeasurementToolButton": {
+ "dataElement": "arcMeasurementToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateArcMeasurement"
+ },
+ "perimeterMeasurementToolButton": {
+ "dataElement": "perimeterMeasurementToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreatePerimeterMeasurement"
+ },
+ "areaMeasurementToolButton": {
+ "dataElement": "areaMeasurementToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateAreaMeasurement"
+ },
+ "ellipseMeasurementToolButton": {
+ "dataElement": "ellipseMeasurementToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateEllipseMeasurement"
+ },
+ "rectangularAreaMeasurementToolButton": {
+ "dataElement": "rectangularAreaMeasurementToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateRectangularAreaMeasurement"
+ },
+ "countMeasurementToolButton": {
+ "dataElement": "countMeasurementToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateCountMeasurement"
+ },
+ "measureGroupedItems": {
+ "dataElement": "measureGroupedItems",
+ "items": [
+ "distanceMeasurementToolButton",
+ "arcMeasurementToolButton",
+ "perimeterMeasurementToolButton",
+ "areaMeasurementToolButton",
+ "ellipseMeasurementToolButton",
+ "rectangularAreaMeasurementToolButton",
+ "countMeasurementToolButton",
+ "divider-0.4",
+ "stylePanelToggle",
+ "defaultAnnotationUtilities"
+ ],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "cropToolButton": {
+ "dataElement": "cropToolButton",
+ "type": "toolButton",
+ "toolName": "CropPage"
+ },
+ "snippingToolButton": {
+ "dataElement": "snippingToolButton",
+ "type": "toolButton",
+ "toolName": "SnippingTool"
+ },
+ "editGroupedItems": {
+ "dataElement": "editGroupedItems",
+ "items": ["cropToolButton", "snippingToolButton"],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "addParagraphToolGroupButton": {
+ "dataElement": "addParagraphToolGroupButton",
+ "type": "toolButton",
+ "toolName": "AddParagraphTool",
+ "disabled": true
+ },
+ "addImageContentToolGroupButton": {
+ "dataElement": "addImageContentToolGroupButton",
+ "type": "toolButton",
+ "toolName": "AddImageContentTool",
+ "disabled": true
+ },
+ "divider-0.6": {
+ "dataElement": "divider-0.6",
+ "type": "divider"
+ },
+ "contentEditButton": {
+ "dataElement": "contentEditButton",
+ "type": "presetButton",
+ "buttonType": "contentEditButton",
+ "disabled": true
+ },
+ "contentEditGroupedItems": {
+ "dataElement": "contentEditGroupedItems",
+ "items": [
+ "addParagraphToolGroupButton",
+ "addImageContentToolGroupButton",
+ "divider-0.6",
+ "contentEditButton"
+ ],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "crossStampToolButton": {
+ "dataElement": "crossStampToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateCrossStamp"
+ },
+ "checkStampToolButton": {
+ "dataElement": "checkStampToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateCheckStamp"
+ },
+ "dotStampToolButton": {
+ "dataElement": "dotStampToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateDotStamp"
+ },
+ "calendarToolButton": {
+ "dataElement": "calendarToolButton",
+ "type": "toolButton",
+ "toolName": "AnnotationCreateDateFreeText"
+ },
+ "fillAndSignGroupedItems": {
+ "dataElement": "fillAndSignGroupedItems",
+ "items": [
+ "signatureCreateToolButton",
+ "freeTextToolButton",
+ "crossStampToolButton",
+ "checkStampToolButton",
+ "dotStampToolButton",
+ "rubberStampToolButton",
+ "calendarToolButton",
+ "divider-0.4",
+ "stylePanelToggle",
+ "defaultAnnotationUtilities"
+ ],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "signatureFieldButton": {
+ "dataElement": "signatureFieldButton",
+ "type": "toolButton",
+ "toolName": "SignatureFormFieldCreateTool"
+ },
+ "textFieldButton": {
+ "dataElement": "textFieldButton",
+ "type": "toolButton",
+ "toolName": "TextFormFieldCreateTool"
+ },
+ "checkboxFieldButton": {
+ "dataElement": "checkboxFieldButton",
+ "type": "toolButton",
+ "toolName": "CheckBoxFormFieldCreateTool"
+ },
+ "radioFieldButton": {
+ "dataElement": "radioFieldButton",
+ "type": "toolButton",
+ "toolName": "RadioButtonFormFieldCreateTool"
+ },
+ "listBoxFieldButton": {
+ "dataElement": "listBoxFieldButton",
+ "type": "toolButton",
+ "toolName": "ListBoxFormFieldCreateTool"
+ },
+ "comboBoxFieldButton": {
+ "dataElement": "comboBoxFieldButton",
+ "type": "toolButton",
+ "toolName": "ComboBoxFormFieldCreateTool"
+ },
+ "divider-0.7": {
+ "dataElement": "divider-0.7",
+ "type": "divider"
+ },
+ "formFieldEditButton": {
+ "dataElement": "formFieldEditButton",
+ "type": "presetButton",
+ "buttonType": "formFieldEditButton"
+ },
+ "divider-0.8": {
+ "dataElement": "divider-0.8",
+ "type": "divider"
+ },
+ "formsToolsGroupedItems": {
+ "dataElement": "formsToolsGroupedItems",
+ "items": [
+ "signatureFieldButton",
+ "textFieldButton",
+ "freeTextToolButton",
+ "checkboxFieldButton",
+ "radioFieldButton",
+ "listBoxFieldButton",
+ "comboBoxFieldButton",
+ "divider-0.7",
+ "formFieldEditButton"
+ ],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "formsGroupedItems": {
+ "dataElement": "formsGroupedItems",
+ "items": [
+ "formsToolsGroupedItems",
+ "divider-0.8",
+ "stylePanelToggle",
+ "indexPanelListToggle"
+ ],
+ "type": "groupedItems",
+ "grow": 0,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "page-controls-container": {
+ "dataElement": "page-controls-container",
+ "type": "pageControls",
+ "title": "component.pageControls",
+ "icon": "icon-page-controls"
+ },
+ "groupedLeftHeaderButtons": {
+ "dataElement": "groupedLeftHeaderButtons",
+ "items": [
+ "menuButton",
+ "divider-0.1",
+ "leftPanelButton",
+ "view-controls",
+ "divider-0.3",
+ "zoom-container",
+ "divider-0.2",
+ "panToolButton",
+ "annotationEditToolButton"
+ ],
+ "type": "groupedItems",
+ "grow": 1,
+ "gap": 12,
+ "alwaysVisible": true
+ },
+ "default-ribbon-group": {
+ "dataElement": "default-ribbon-group",
+ "items": [
+ "toolbarGroup-View",
+ "toolbarGroup-Annotate",
+ "toolbarGroup-Shapes",
+ "toolbarGroup-Insert",
+ "toolbarGroup-Measure",
+ "toolbarGroup-Redact",
+ "toolbarGroup-Edit",
+ "toolbarGroup-EditText",
+ "toolbarGroup-FillAndSign",
+ "toolbarGroup-Forms"
+ ],
+ "type": "ribbonGroup",
+ "justifyContent": "start",
+ "grow": 2,
+ "gap": 12,
+ "alwaysVisible": false
+ },
+ "comparePanelToggle": {
+ "dataElement": "comparePanelToggle",
+ "title": "action.comparePages",
+ "disabled": true,
+ "type": "presetButton",
+ "label": "action.comparePages",
+ "buttonType": "compareButton"
+ },
+ "searchPanelToggle": {
+ "dataElement": "searchPanelToggle",
+ "title": "component.searchPanel",
+ "disabled": true,
+ "type": "toggleButton",
+ "img": "icon-header-search",
+ "toggleElement": "searchPanel"
+ },
+ "notesPanelToggle": {
+ "dataElement": "notesPanelToggle",
+ "title": "component.notesPanel",
+ "disabled": true,
+ "type": "toggleButton",
+ "img": "icon-header-chat-line",
+ "toggleElement": "notesPanel"
+ },
+ "pan-tool-2": {
+ "dataElement": "pan-tool-2",
+ "title": "Pan the document",
+ "type": "toolButton",
+ "label": "Pan",
+ "img": "icon-header-pan",
+ "toolName": "Pan"
+ },
+ "newDocumentButton": {
+ "dataElement": "newDocumentButton",
+ "presetDataElement": "newDocumentPresetButton",
+ "label": "action.newDocument",
+ "title": "action.newDocument",
+ "isActive": false,
+ "type": "presetButton",
+ "buttonType": "newDocumentButton"
+ },
+ "fullscreenButton": {
+ "dataElement": "fullscreenButton",
+ "presetDataElement": "fullscreenPresetButton",
+ "label": "action.enterFullscreen",
+ "title": "action.enterFullscreen",
+ "type": "presetButton",
+ "buttonType": "fullscreenButton",
+ "disabled": true
+ }
+ },
+ "modularHeaders": {
+ "default-top-header": {
+ "dataElement": "default-top-header",
+ "placement": "top",
+ "grow": 0,
+ "gap": 12,
+ "position": "start",
+ "float": false,
+ "stroke": true,
+ "dimension": {
+ "paddingTop": 8,
+ "paddingBottom": 8,
+ "borderWidth": 1
+ },
+ "style": {},
+ "items": [
+ "groupedLeftHeaderButtons",
+ "default-ribbon-group",
+ "comparePanelToggle",
+ "searchPanelToggle",
+ "notesPanelToggle"
+ ]
+ },
+ "tools-header": {
+ "dataElement": "tools-header",
+ "placement": "top",
+ "justifyContent": "center",
+ "grow": 0,
+ "gap": 12,
+ "position": "end",
+ "float": false,
+ "stroke": true,
+ "dimension": {
+ "paddingTop": 8,
+ "paddingBottom": 8,
+ "borderWidth": 1
+ },
+ "style": {},
+ "items": [
+ "annotateGroupedItems",
+ "shapesGroupedItems",
+ "insertGroupedItems",
+ "redactionGroupedItems",
+ "measureGroupedItems",
+ "editGroupedItems",
+ "contentEditGroupedItems",
+ "fillAndSignGroupedItems",
+ "formsGroupedItems"
+ ]
+ },
+ "page-nav-floating-header": {
+ "dataElement": "page-nav-floating-header",
+ "placement": "bottom",
+ "grow": 0,
+ "gap": 12,
+ "position": "center",
+ "opacityMode": "dynamic",
+ "opacity": "full",
+ "float": true,
+ "stroke": true,
+ "dimension": {
+ "paddingTop": 8,
+ "paddingBottom": 8,
+ "borderWidth": 1
+ },
+ "style": {
+ "background": "var(--gray-1)",
+ "padding": "8px",
+ "borderStyle": "solid",
+ "borderWidth": 1,
+ "borderColor": "var(--gray-5)"
+ },
+ "items": ["page-controls-container"]
+ }
+ },
+ "panels": {
+ "comparePanel": {
+ "dataElement": "comparePanel",
+ "render": "changeListPanel",
+ "location": "end"
+ },
+ "stylePanel": {
+ "dataElement": "stylePanel",
+ "render": "stylePanel",
+ "location": "start"
+ },
+ "thumbnailsPanel": {
+ "dataElement": "thumbnailsPanel",
+ "render": "thumbnailsPanel",
+ "location": "start"
+ },
+ "outlinesPanel": {
+ "dataElement": "outlinesPanel",
+ "render": "outlinesPanel",
+ "location": "start"
+ },
+ "bookmarksPanel": {
+ "dataElement": "bookmarksPanel",
+ "render": "bookmarksPanel",
+ "location": "start"
+ },
+ "formFieldPanel": {
+ "dataElement": "formFieldPanel",
+ "render": "formFieldPanel",
+ "location": "end"
+ },
+ "indexPanel": {
+ "dataElement": "indexPanel",
+ "render": "indexPanel",
+ "location": "end"
+ },
+ "layersPanel": {
+ "dataElement": "layersPanel",
+ "render": "layersPanel",
+ "location": "start"
+ },
+ "signatureListPanel": {
+ "dataElement": "signatureListPanel",
+ "render": "signatureListPanel",
+ "location": "start"
+ },
+ "fileAttachmentPanel": {
+ "dataElement": "fileAttachmentPanel",
+ "render": "fileAttachmentPanel",
+ "location": "start"
+ },
+ "rubberStampPanel": {
+ "dataElement": "rubberStampPanel",
+ "render": "rubberStampPanel",
+ "location": "start"
+ },
+ "textEditingPanel": {
+ "dataElement": "textEditingPanel",
+ "render": "textEditingPanel",
+ "location": "end"
+ },
+ "signaturePanel": {
+ "dataElement": "signaturePanel",
+ "render": "signaturePanel",
+ "location": "start",
+ "disabled": true
+ },
+ "portfolioPanel": {
+ "dataElement": "portfolioPanel",
+ "render": "portfolioPanel",
+ "location": "start",
+ "disabled": true
+ },
+ "tabPanel": {
+ "render": "tabPanel",
+ "dataElement": "tabPanel",
+ "panelsList": [
+ {
+ "render": "thumbnailsPanel"
+ },
+ {
+ "render": "outlinesPanel"
+ },
+ {
+ "render": "bookmarksPanel"
+ },
+ {
+ "render": "layersPanel"
+ },
+ {
+ "render": "signaturePanel"
+ },
+ {
+ "render": "fileAttachmentPanel"
+ },
+ {
+ "render": "portfolioPanel"
+ }
+ ],
+ "location": "start"
+ },
+ "notesPanel": {
+ "dataElement": "notesPanel",
+ "render": "notesPanel",
+ "location": "end"
+ },
+ "searchPanel": {
+ "dataElement": "searchPanel",
+ "render": "searchPanel",
+ "location": "end"
+ },
+ "redactionPanel": {
+ "dataElement": "redactionPanel",
+ "render": "redactionPanel",
+ "location": "end"
+ }
+ },
+ "flyouts": {
+ "MainMenuFlyout": {
+ "dataElement": "MainMenuFlyout",
+ "items": [
+ "newDocumentButton",
+ "filePickerButton",
+ "downloadButton",
+ "fullscreenButton",
+ "saveAsButton",
+ "printButton",
+ "divider",
+ "createPortfolioButton",
+ "divider",
+ "settingsButton",
+ "divider"
+ ]
+ },
+ "multiSelectStylePanelFlyout": {
+ "dataElement": "multiSelectStylePanelFlyout",
+ "className": "StylePanelFlyout",
+ "items": [
+ {
+ "dataElement": "stylePanelInFlyout",
+ "render": "stylePanel"
+ }
+ ]
+ }
+ },
+ "popups": {
+ "annotationPopup": [
+ {
+ "dataElement": "viewFileButton"
+ },
+ {
+ "dataElement": "annotationCommentButton"
+ },
+ {
+ "dataElement": "annotationStyleEditButton"
+ },
+ {
+ "dataElement": "annotationDateEditButton"
+ },
+ {
+ "dataElement": "annotationRedactButton"
+ },
+ {
+ "dataElement": "annotationCropButton"
+ },
+ {
+ "dataElement": "annotationContentEditButton"
+ },
+ {
+ "dataElement": "annotationClearSignatureButton"
+ },
+ {
+ "dataElement": "annotationGroupButton"
+ },
+ {
+ "dataElement": "annotationUngroupButton"
+ },
+ {
+ "dataElement": "formFieldEditButton"
+ },
+ {
+ "dataElement": "calibratePopupButton"
+ },
+ {
+ "dataElement": "linkButton"
+ },
+ {
+ "dataElement": "fileAttachmentDownload"
+ },
+ {
+ "dataElement": "annotationDeleteButton"
+ },
+ {
+ "dataElement": "shortCutKeysFor3D"
+ },
+ {
+ "dataElement": "playSoundButton"
+ },
+ {
+ "dataElement": "openAlignmentButton"
+ }
+ ],
+ "textPopup": [
+ {
+ "dataElement": "copyTextButton"
+ },
+ {
+ "dataElement": "textHighlightToolButton"
+ },
+ {
+ "dataElement": "textUnderlineToolButton"
+ },
+ {
+ "dataElement": "textSquigglyToolButton"
+ },
+ {
+ "dataElement": "textStrikeoutToolButton"
+ },
+ {
+ "dataElement": "textRedactToolButton"
+ },
+ {
+ "dataElement": "linkButton"
+ }
+ ],
+ "contextMenuPopup": [
+ {
+ "dataElement": "panToolButton"
+ },
+ {
+ "dataElement": "stickyToolButton"
+ },
+ {
+ "dataElement": "highlightToolButton"
+ },
+ {
+ "dataElement": "freeHandToolButton"
+ },
+ {
+ "dataElement": "freeHandHighlightToolButton"
+ },
+ {
+ "dataElement": "freeTextToolButton"
+ },
+ {
+ "dataElement": "markInsertTextToolButton"
+ },
+ {
+ "dataElement": "markReplaceTextToolButton"
+ }
+ ]
+ }
+}
diff --git a/webviewer-redaction-ai/client/ui/functionMap.js b/webviewer-redaction-ai/client/ui/functionMap.js
new file mode 100644
index 0000000..7cd5d62
--- /dev/null
+++ b/webviewer-redaction-ai/client/ui/functionMap.js
@@ -0,0 +1,13 @@
+import { analyzeDocumentForPII } from '../document/analyzer.js';
+import { applyRedactions } from '../redaction.js';
+
+const functionMap = {
+ 'AIPIIRedactionClick': async () => {
+ const analysisSuccess = await analyzeDocumentForPII();
+ if (analysisSuccess) {
+ await applyRedactions();
+ }
+ },
+};
+
+export default functionMap;
\ No newline at end of file
diff --git a/webviewer-redaction-ai/mainsamplesource.json b/webviewer-redaction-ai/mainsamplesource.json
new file mode 100644
index 0000000..ffb06aa
--- /dev/null
+++ b/webviewer-redaction-ai/mainsamplesource.json
@@ -0,0 +1,65 @@
+{
+ "githubId": "f4c122ff-0b65-444f-9a25-73b089c62cfd",
+ "files": [
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/client/index.js",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/client/globals.js",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/client/redaction.js",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/client/document/analyzer.js",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/client/document/manager.js",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/client/ui/custom.json",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/client/ui/functionMap.js",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/server/config.json",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/server/handler.js",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/server/inMemoryStore.js",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/server/llmManager.js",
+ "description": "",
+ "reason": ""
+ },
+ {
+ "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-redaction-ai/server/server.js",
+ "description": "",
+ "reason": ""
+ }
+ ]
+}
diff --git a/webviewer-redaction-ai/package-lock.json b/webviewer-redaction-ai/package-lock.json
new file mode 100644
index 0000000..7855274
--- /dev/null
+++ b/webviewer-redaction-ai/package-lock.json
@@ -0,0 +1,1453 @@
+{
+ "name": "webviewer-redaction-ai",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "webviewer-redaction-ai",
+ "version": "1.0.0",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@langchain/core": "^1.1.32",
+ "@langchain/openai": "^1.2.13",
+ "@pdftron/webviewer": "^11.11.0",
+ "dotenv": "^17.3.1",
+ "express": "^5.2.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.58.2",
+ "body-parser": "^2.2.2",
+ "fs-extra": "^11.3.4",
+ "open": "^11.0.0"
+ }
+ },
+ "node_modules/@cfworker/json-schema": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
+ "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
+ "license": "MIT"
+ },
+ "node_modules/@langchain/core": {
+ "version": "1.1.36",
+ "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.36.tgz",
+ "integrity": "sha512-9NWsdzU3uZD13lJwunXK0t6SIwew+UwcbHggW5yUdaiMmzKeNkDpp1lRD6p49N8+D0Vv4qmQBEKB4Ukh2jfnvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@cfworker/json-schema": "^4.0.2",
+ "@standard-schema/spec": "^1.1.0",
+ "ansi-styles": "^5.0.0",
+ "camelcase": "6",
+ "decamelize": "1.2.0",
+ "js-tiktoken": "^1.0.12",
+ "langsmith": ">=0.5.0 <1.0.0",
+ "mustache": "^4.2.0",
+ "p-queue": "^6.6.2",
+ "uuid": "^11.1.0",
+ "zod": "^3.25.76 || ^4"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@langchain/openai": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.3.1.tgz",
+ "integrity": "sha512-6yN3XFRUKUsGREGk4VtCvnMp5NHh2gWujiuWdn/G7cCeHboYrdKLWnwGqopuFOm7Tivv423gtMN1GQ7EJ3kg+g==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tiktoken": "^1.0.12",
+ "openai": "^6.27.0",
+ "zod": "^3.25.76 || ^4"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@langchain/core": "^1.1.36"
+ }
+ },
+ "node_modules/@pdftron/webviewer": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-11.11.0.tgz",
+ "integrity": "sha512-KHF7ldPGOV7wpyRaAWennKVJsv6ZbGBrzD4uDsXu7UR8XGbZRCE5QpD/0S2xyKEMoLmmmUrASy6RHYts91fAsQ=="
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
+ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "license": "MIT"
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/bundle-name": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/console-table-printer": {
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz",
+ "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==",
+ "license": "MIT",
+ "dependencies": {
+ "simple-wcswidth": "^1.1.2"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/default-browser": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
+ "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
+ "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.3.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
+ "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "license": "MIT"
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "11.3.4",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
+ "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-in-ssh": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
+ "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-wsl": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
+ "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/js-tiktoken": {
+ "version": "1.0.21",
+ "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
+ "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.5.1"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/langsmith": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.5.13.tgz",
+ "integrity": "sha512-arD4XzLdMTyb3rrKVainpXG9vtJPL3urOc/fC/yMHeoorK5KSHsvBWkogZUgjCsLkhtH8FdfSEAAG8Am8DNoYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/uuid": "^10.0.0",
+ "chalk": "^5.6.2",
+ "console-table-printer": "^2.12.1",
+ "p-queue": "^6.6.2",
+ "semver": "^7.6.3",
+ "uuid": "^10.0.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "*",
+ "@opentelemetry/exporter-trace-otlp-proto": "*",
+ "@opentelemetry/sdk-trace-base": "*",
+ "openai": "*",
+ "ws": ">=7"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@opentelemetry/exporter-trace-otlp-proto": {
+ "optional": true
+ },
+ "@opentelemetry/sdk-trace-base": {
+ "optional": true
+ },
+ "openai": {
+ "optional": true
+ },
+ "ws": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/langsmith/node_modules/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mustache": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
+ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
+ "license": "MIT",
+ "bin": {
+ "mustache": "bin/mustache"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/open": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz",
+ "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^5.4.0",
+ "define-lazy-prop": "^3.0.0",
+ "is-in-ssh": "^1.0.0",
+ "is-inside-container": "^1.0.0",
+ "powershell-utils": "^0.1.0",
+ "wsl-utils": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/openai": {
+ "version": "6.32.0",
+ "resolved": "https://registry.npmjs.org/openai/-/openai-6.32.0.tgz",
+ "integrity": "sha512-j3k+BjydAf8yQlcOI7WUQMQTbbF5GEIMAE2iZYCOzwwB3S2pCheaWYp+XZRNAch4jWVc52PMDGRRjutao3lLCg==",
+ "license": "Apache-2.0",
+ "bin": {
+ "openai": "bin/cli"
+ },
+ "peerDependencies": {
+ "ws": "^8.18.0",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "ws": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-queue": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
+ "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.4",
+ "p-timeout": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-timeout": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
+ "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
+ "license": "MIT",
+ "dependencies": {
+ "p-finally": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
+ "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/powershell-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
+ "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/run-applescript": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+ "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-wcswidth": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
+ "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/wsl-utils": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz",
+ "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-wsl": "^3.1.0",
+ "powershell-utils": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/webviewer-redaction-ai/package.json b/webviewer-redaction-ai/package.json
new file mode 100644
index 0000000..aab8bb4
--- /dev/null
+++ b/webviewer-redaction-ai/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "webviewer-redaction-ai",
+ "version": "1.0.0",
+ "description": "",
+ "type": "module",
+ "main": "index.js",
+ "scripts": {
+ "postinstall": "node tools/copy-webviewer-files.js",
+ "start": "node server/server.js",
+ "test:e2e": "npx playwright test",
+ "test:e2e:ui": "npx playwright test --ui",
+ "test:e2e:debug": "npx playwright test --debug"
+ },
+ "author": "Apryse Systems Inc.",
+ "devDependencies": {
+ "@playwright/test": "^1.58.2",
+ "body-parser": "^2.2.2",
+ "fs-extra": "^11.3.4",
+ "open": "^11.0.0"
+ },
+ "dependencies": {
+ "@langchain/core": "^1.1.32",
+ "@langchain/openai": "^1.2.13",
+ "@pdftron/webviewer": "^11.11.0",
+ "dotenv": "^17.3.1",
+ "express": "^5.2.1"
+ }
+}
diff --git a/webviewer-redaction-ai/playwright.config.js b/webviewer-redaction-ai/playwright.config.js
new file mode 100644
index 0000000..308b074
--- /dev/null
+++ b/webviewer-redaction-ai/playwright.config.js
@@ -0,0 +1,22 @@
+// Playwright configuration for E2E tests
+// See https://playwright.dev/docs/test-configuration
+
+import { defineConfig } from '@playwright/test';
+
+export default defineConfig({
+ testDir: '__tests__',
+ fullyParallel: true,
+ reporter: 'html',
+ retries: 0,
+ use: {
+ headless: true,
+ baseURL: 'http://localhost:4040/',
+ screenshot: 'only-on-failure',
+ video: 'retain-on-failure',
+ },
+ webServer: {
+ command: 'npm start -- --no-open',
+ url: 'http://localhost:4040/client/index.html',
+ reuseExistingServer: true,
+ },
+});
\ No newline at end of file
diff --git a/webviewer-redaction-ai/sample.mp4 b/webviewer-redaction-ai/sample.mp4
new file mode 100644
index 0000000..9826b24
Binary files /dev/null and b/webviewer-redaction-ai/sample.mp4 differ
diff --git a/webviewer-redaction-ai/server/config.json b/webviewer-redaction-ai/server/config.json
new file mode 100644
index 0000000..ce60790
--- /dev/null
+++ b/webviewer-redaction-ai/server/config.json
@@ -0,0 +1,52 @@
+{
+ "guardRail": {
+ "assistantPrompt": "From the given text, extract the following information: PIICLASSIFICATIONSPLACEHOLDER and apply the following rules: ",
+ "piiClassifications": [
+ {
+ "classification": "personal",
+ "details": ["person names"]
+ },
+ {
+ "classification": "contact",
+ "details": [
+ "phone numbers (landline, mobile, fax)",
+ "email addresses",
+ "physical addresses (street, city, state, zip code)",
+ "mailing addresses (P.O. Box)"
+ ]
+ },
+ {
+ "classification": "financial",
+ "details": [
+ "credit card numbers",
+ "bank account numbers",
+ "IBAN codes",
+ "SWIFT/BIC codes"
+ ]
+ },
+ {
+ "classification": "governmental",
+ "details": [
+ "national ID numbers",
+ "passport numbers",
+ "social security numbers",
+ "driver's license numbers",
+ "tax ID numbers"
+ ]
+ }
+ ],
+ "rulesSet": [
+ "1. Response must be in form of list.",
+ "2. Response must not be numbered using numbers, bullet points, hyphens/dashes.",
+ "3. Response must not be labeled (e.g., 'Name: Linda Nelson', 'Phone: 291 555 5555', 'Address: 18th Floor, 2822 Glenoaks St.', etc.).",
+ "4. Response must not contain duplicates.",
+ "5. If a punctuated address or an address appears on separate lines in the original text is recognized, list each as its own line in the same order they appear.",
+ "6. If P.O Box, IBAN, SWIFT/BIC, National ID, passport, social security, driver's license, and tax ID numbers include whitespace characters, returns nothing.",
+ "7. If no finding is recognized, returns nothing.",
+ "8. If single word is recognized, returns nothing.",
+ "9. If multiple findings are recognized, and appear on the same line or punctuated in the original text, they must be listed separately.",
+ "10. If a field name finding is recognized (e.g., Social Security Number or Social Security No.), returns nothing.",
+ "11. Legal entity names that look like person names (e.g., Peady, Eff, & Wright Exporting; Ace Deekay Shipping; Kroft & Partners) should be excluded from the response, but any associated information (e.g., phone numbers, addresses, person names, etc.) should still be extracted and listed."
+ ]
+ }
+}
\ No newline at end of file
diff --git a/webviewer-redaction-ai/server/handler.js b/webviewer-redaction-ai/server/handler.js
new file mode 100644
index 0000000..c5287e8
--- /dev/null
+++ b/webviewer-redaction-ai/server/handler.js
@@ -0,0 +1,164 @@
+import { HumanMessage, SystemMessage } from '@langchain/core/messages';
+import LLMManager from './llmManager.js';
+import InMemoryStore from './inMemoryStore.js';
+import dotenv from 'dotenv';
+import { readFileSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+dotenv.config();
+
+// Load configuration
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const configPath = join(__dirname, './config.json');
+const config = JSON.parse(readFileSync(configPath, 'utf8'));
+const { guardRail } = config;
+
+// Create LLMManager instance
+const llmManager = new LLMManager();
+// Create InMemoryStore instance to validate the
+// received document text length and page count
+// are within acceptable limits to prevent processing
+// of excessively large documents.
+const inMemoryStore = new InMemoryStore();
+// Variables to hold document text, validation status, and analysis results in memory for quick access during the request lifecycle.
+let documentText = '';
+let isDocumentValid = false;
+let analysisData = null;
+// Clear document and analysis data.
+const cleanupData = () => {
+ documentText = '';
+ isDocumentValid = false;
+ analysisData = null;
+}
+
+export default function registerHandlers(app) {
+ // Initialize LangChain on startup
+ llmManager.initialize();
+
+ // Prepare assistant prompt with PII classifications and guardrail rules
+ let prepareAssistantPrompt = (guardRail) => {
+ let prompt = guardRail.assistantPrompt.replace('PIICLASSIFICATIONSPLACEHOLDER', guardRail.piiClassifications.flatMap(item => item.details).join(', '));
+ prompt += guardRail.rulesSet.map(rule => `${rule}`).join(' ');
+ return prompt;
+ };
+
+ // Create system prompt message
+ let systemMessage = prepareAssistantPrompt(guardRail);
+ systemMessage = new SystemMessage(systemMessage);
+
+ // Endpoint to receive document text from client
+ app.post('/api/send-text', async (request, response) => {
+ try {
+ if (!llmManager.isInitialized()) {
+ return response.status(200).json({
+ error: 'LangChain not available or missing OPENAI_API_KEY in .env file.',
+ success: false
+ });
+ }
+
+ const state = inMemoryStore.isValidDocument(request.body.documentText.trim(), request.body.pageCount);
+ switch (state) {
+ case 0:
+ return response.status(200).json({
+ error: 'Document text is empty.',
+ success: false
+ });
+ case 1:
+ return response.status(200).json({
+ error: `Document text size exceeds ${InMemoryStore.MAX_DOCUMENT_LENGTH} characters limit.`,
+ success: false
+ });
+ case 2:
+ return response.status(200).json({
+ error: `Document pages number exceeds ${InMemoryStore.ALLOWED_PAGES} pages limit.`,
+ success: false
+ });
+ default:
+ documentText = request.body.documentText.trim();
+ isDocumentValid = true;
+ response.json({
+ message: 'Document text received successfully.',
+ success: true
+ });
+ }
+ } catch (error) {
+ console.error('Error receiving document text:', error);
+ cleanupData();
+ response.status(500).json({
+ error: 'Failed to receive document text',
+ details: error.message,
+ success: false
+ });
+ }
+ });
+
+ // Endpoint to analyze document for PII
+ app.post('/api/analyze-pii', async (request, response) => {
+ try {
+ // Check if LangChain is ready for AI processing
+ if (!llmManager.isInitialized()) {
+ cleanupData();
+ return response.status(500).json({
+ error: 'LangChain not available. Please check server configuration.',
+ success: false
+ });
+ }
+
+ // Check if document is valid before analysis
+ if (!isDocumentValid) {
+ cleanupData();
+ return response.status(400).json({
+ error: 'Invalid document. Please check the document text and page count.',
+ success: false
+ });
+ }
+
+ // AI processing with LangChain for PII detection
+ const humanMessage = new HumanMessage(documentText);
+ analysisData = await llmManager.executeMessages([systemMessage, humanMessage]);
+
+ response.json({
+ success: true,
+ message: 'PII analysis completed'
+ });
+ } catch (error) {
+ console.error('Error analyzing document for PII:', error);
+ cleanupData();
+ response.status(500).json({
+ error: 'Failed to analyze document for PII',
+ details: error.message,
+ success: false
+ });
+ }
+ });
+
+ // Endpoint to send results back to client
+ app.get('/api/get-results', async (request, response) => {
+ try {
+ // Check if analysis data is available
+ if (!analysisData)
+ return response.status(200).json({
+ error: 'No analysis results found. Please analyze the document first.',
+ success: false
+ });
+
+ response.status(200).json({
+ success: true,
+ analysis: analysisData
+ });
+ } catch (error) {
+ console.error('Error retrieving analysis results:', error);
+ response.status(500).json({
+ error: 'Failed to retrieve analysis results',
+ details: error.message,
+ success: false
+ });
+ }
+ finally {
+ // Clear document and analysis data after sending results
+ cleanupData();
+ }
+ });
+}
\ No newline at end of file
diff --git a/webviewer-redaction-ai/server/inMemoryStore.js b/webviewer-redaction-ai/server/inMemoryStore.js
new file mode 100644
index 0000000..2c02f69
--- /dev/null
+++ b/webviewer-redaction-ai/server/inMemoryStore.js
@@ -0,0 +1,20 @@
+// InMemoryStore class to validate the received document text length
+// and page count are within acceptable limits to prevent processing
+// of excessively large documents.
+export default class InMemoryStore {
+ static ALLOWED_PAGES = 20;
+ static MAX_DOCUMENT_LENGTH = 30000;
+
+ isValidDocument(documentText, pageCount) {
+ if (!documentText || documentText.length === 0)
+ return 0;
+
+ if (documentText.length > InMemoryStore.MAX_DOCUMENT_LENGTH)
+ return 1;
+
+ if (pageCount > InMemoryStore.ALLOWED_PAGES)
+ return 2;
+
+ return -1;
+ }
+}
diff --git a/webviewer-redaction-ai/server/llmManager.js b/webviewer-redaction-ai/server/llmManager.js
new file mode 100644
index 0000000..c0cc9c5
--- /dev/null
+++ b/webviewer-redaction-ai/server/llmManager.js
@@ -0,0 +1,72 @@
+import { ChatOpenAI } from '@langchain/openai';
+import { StringOutputParser } from '@langchain/core/output_parsers';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+// LLMManager - Manages LLM initialization, configuration, and execution
+// Handles OpenAI chat models, token counting, and text processing
+class LLMManager {
+ llm = null;
+ parser = null;
+
+ // Initialize LangChain components (LLM and parser)
+ initialize() {
+ if (!process.env.OPENAI_API_KEY) {
+ console.error('Missing OPENAI_API_KEY in .env file');
+ return;
+ }
+
+ try {
+ this.llm = new ChatOpenAI({
+ apiKey: process.env.OPENAI_API_KEY,
+ modelName: process.env.OPENAI_MODEL,
+ maxTokens: Number.parseInt(process.env.OPENAI_MAX_TOKENS, 10),
+ temperature: Number.parseFloat(process.env.OPENAI_TEMPERATURE)
+ });
+
+ this.parser = new StringOutputParser();
+
+ if (!this.llm || !this.parser) {
+ console.error('Failed to initialize LangChain components');
+ return;
+ }
+
+ console.log('LangChain initialized successfully');
+ } catch (error) {
+ console.error('Failed to initialize LangChain components:', error);
+ }
+ }
+
+ // Check LangChain initialization status
+ // @returns {boolean} True if initialized, false otherwise
+ isInitialized() {
+ return !!this.llm && !!this.parser;
+ }
+
+ // Execute messages with LLM and parse response
+ // @param {Array} messages - Array of message objects
+ // @returns {Promise} Parsed response or null if failed
+ async executeMessages(messages) {
+ if (!this.isInitialized()) {
+ console.error('Unable to execute messages - LangChain components are not initialized');
+ return null;
+ }
+
+ const response = await this.llm.invoke(messages);
+ if (!response) {
+ console.error('LLM invocation returned no response');
+ return null;
+ }
+
+ const parsedResponse = await this.parser.parse(response.content);
+ if (!parsedResponse) {
+ console.error('Parsing LLM response returned no result');
+ return null;
+ }
+
+ return parsedResponse;
+ }
+}
+
+export default LLMManager;
\ No newline at end of file
diff --git a/webviewer-redaction-ai/server/server.js b/webviewer-redaction-ai/server/server.js
new file mode 100644
index 0000000..8e2bf9e
--- /dev/null
+++ b/webviewer-redaction-ai/server/server.js
@@ -0,0 +1,38 @@
+// This file is to run a server in localhost:process.env.PORT
+import express from 'express';
+import bodyParser from 'body-parser';
+import open from 'open';
+import handler from './handler.js';
+import dotenv from 'dotenv';
+
+dotenv.config();
+const port = Number(process.env.PORT) || 4040;
+// For testing purposes only, you can prevent the server from
+// opening the browser automatically when it starts by either:
+// - setting the environment variable NO_OPEN=true, or
+// - passing the CLI flag --no-open
+const noOpenEnv = String(process.env.NO_OPEN || '').toLowerCase() === 'true';
+const noOpenFlag = process.argv.includes('--no-open');
+const shouldOpenBrowser = !(noOpenEnv || noOpenFlag);
+
+// Use JSON body parser for API endpoints
+const app = express();
+app.use(bodyParser.json());
+app.use(bodyParser.text());
+
+// For statically serving 'client' folder
+app.use('/client', express.static('client'));
+
+handler(app);
+
+// Run server
+app.listen(port, 'localhost', (err) => {
+ if (err) {
+ console.error(err);
+ } else {
+ console.info(`Server is listening at http://localhost:${port}/client/index.html`);
+ if (shouldOpenBrowser) {
+ open(`http://localhost:${port}/client/index.html`);
+ }
+ }
+});
\ No newline at end of file
diff --git a/webviewer-redaction-ai/tools/copy-webviewer-files.js b/webviewer-redaction-ai/tools/copy-webviewer-files.js
new file mode 100644
index 0000000..dc38b63
--- /dev/null
+++ b/webviewer-redaction-ai/tools/copy-webviewer-files.js
@@ -0,0 +1,13 @@
+import fs from 'fs-extra';
+
+const copyFiles = async () => {
+ try {
+ await fs.copy('./node_modules/@pdftron/webviewer/public', './client/lib');
+ await fs.copy('./node_modules/@pdftron/webviewer/webviewer.min.js', './client/lib/webviewer.min.js');
+ console.log('WebViewer files copied over successfully');
+ } catch (err) {
+ console.error(err);
+ }
+};
+
+copyFiles();
\ No newline at end of file