Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


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

- [webviewer-ask-ai](./webviewer-ask-ai) - Integrate WebViewer with Artificial Intelligence
- [webviewer-salesforce](./webviewer-salesforce) - Integrate WebViewer in Salesforce
- [webviewer-salesforce-attachments](./webviewer-salesforce-attachments) - View Salesforce record attachments in WebViewer
- [webviewer-mendix](./webviewer-mendix) - Integrate WebViewer into a Mendix low-code app
Expand Down
1 change: 1 addition & 0 deletions lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"webviewer-annotations-nodejs",
"webviewer-annotations-php",
"webviewer-annotations-sqlite3",
"webviewer-ask-ai",
"webviewer-barcode",
"webviewer-blazor",
"webviewer-blazor-wasm",
Expand Down
10 changes: 10 additions & 0 deletions webviewer-ask-ai/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DOTENV_CONFIG_QUIET=true

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

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

# WebViewer
client/lib
client/license-key.js
2 changes: 2 additions & 0 deletions webviewer-ask-ai/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Copyright (c) 2025 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).
45 changes: 45 additions & 0 deletions webviewer-ask-ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# WebViewer - Ask AI sample

[WebViewer](https://docs.apryse.com/web/guides/get-started) 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/)

This sample demonstrates how to utilize the artificial intelligence capabilities within the WebViewer, using a chat panel interface to ask questions about the loaded document. Also the user can select a text in the document that can be summarized.

## Get your trial key

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-ask-ai
npm install
```

## Configuration

This sample uses OpenAI. You can use any other artificial intelligence of your choice.

However, to get started with this sample rename `.env.example` file into `.env` and fill the followings:

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

## Run

```
npm start
```

This will start a server that you can access the WebViewer client at http://localhost:4040/client/index.html, and the connection to the OpenAI will be managed on backend.
13 changes: 13 additions & 0 deletions webviewer-ask-ai/client/assets/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
204 changes: 204 additions & 0 deletions webviewer-ask-ai/client/chatbot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Browser-compatible chatbot client
class ChatbotClient {
constructor() {
this.conversationHistory = [];
}

// Initialize chat interface for the WebViewer panel
initialize = () => {
// You can expand this to integrate with the WebViewer panel UI
window.chatbot = this; // Make chatbot available globally for testing
};

async sendMessage(promptLine, message, options = {}) {
try {
// For document-level operations, optionally use empty history to prevent token overflow
const historyToSend = options.useEmptyHistory ? [] : this.conversationHistory;
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: message,
promptType: promptLine,
history: historyToSend
})
});

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

const data = await response.json();

// Update conversation history only if not explicitly disabled
if (!options.skipHistoryUpdate) {
this.conversationHistory.push(
{ role: 'human', content: `${promptLine}: ${message.substring(0, 100)}...` }, // Truncate long messages in history
{ role: 'assistant', content: data.response }
);
}

return data.response;
} catch (error) {
throw error;
}
}

//Combine into single container for all bubble responses
getAllText = async (promptType, createBubble) => {
const doc = window.WebViewer.getInstance().Core.documentViewer.getDocument();
doc.getDocumentCompletePromise().then(async () => {

const pageCount = doc.getPageCount();
const pageTexts = new Array(pageCount);
let loadedPages = 0;

// Load all pages and store them in correct order
for (let i = 1; i <= pageCount; i++) {
try {
const text = await doc.loadPageText(i);
// Store with page break BEFORE the content
pageTexts[i - 1] = `<<PAGE_BREAK>> Page ${i}\n${text}`;
loadedPages++;

// When all pages are loaded, combine and send
if (loadedPages === pageCount) {
const completeText = pageTexts.join('\n\n');

// Use empty history for document-level operations to prevent token overflow
this.sendMessage(promptType, completeText, {
useEmptyHistory: true,
skipHistoryUpdate: false // Still update history but with truncated content
}).then(response => {
let responseText = this.responseText(response);
responseText = this.formatText(promptType, responseText);
createBubble(responseText, 'assistant');
}).catch(error => {
createBubble(`Error: ${error.message}`, 'assistant');
});
}
} catch (error) {
pageTexts[i - 1] = `<<PAGE_BREAK>> Page ${i}\n[Error loading page content]`;
loadedPages++;

// Still proceed if all pages are processed (even with errors)
if (loadedPages === pageCount) {
const completeText = pageTexts.join('\n\n');

this.sendMessage(promptType, completeText, {
useEmptyHistory: true,
skipHistoryUpdate: false
}).then(response => {
let responseText = this.responseText(response);
responseText = this.formatText(promptType, responseText);
createBubble(responseText, 'assistant');
}).catch(error => {
createBubble(`Error: ${error.message}`, 'assistant');
});
}
}
}
});
};

// Extract text from OpenAI response via LangChain
responseText = (response) => {
// Primary: Server should send clean string content
if (typeof response === 'string') {
return response;
}

// Fallback: if server still sends complex object, extract properly
if (typeof response === 'object' && response !== null) {

// Standard LangChain approach: use .content property directly
if (response.content !== undefined) {
return response.content;
}

// Fallback for serialized LangChain objects
if (response.kwargs && response.kwargs.content) {
return response.kwargs.content;
}

return JSON.stringify(response);
}

return 'No response received';
};

// Format text to include cited page links
// and page breaks based on prompt type
formatText = (promptType, text) => {
switch (promptType) {
case 'DOCUMENT_SUMMARY':
case 'SELECTED_TEXT_SUMMARY':
case 'DOCUMENT_QUESTION':
// Add page breaks to page citation ends with period
text = text.replace(/(\d+\])\./g, '$1.<br/><br/>');
break;
case 'DOCUMENT_KEYWORDS':
// Format bullet points with line breaks
let lines = text.split(/•\s*/).filter(Boolean);
text = lines.map(line => `• ${line.trim()}`).join('<br/>');
break;
default:
break;
}

// Separate citations group on form [1, 2, 3] to individual [1][2][3]
text = this.separateGroupedCitations(text, /\[\d+(?:\s*,\s*\d+)+\]/g);

// Separate citations range on form [1-3] to individual [1][2][3]
text = this.separateGroupedCitations(text, /\[\d+(?:\s*-\s*\d+)+\]/g);

let matches = text.match(/\[\d+\]/g);
if (matches && matches.length > 0) {
// Element duplicate matches
matches = [...new Set(matches)];

let pageNumber = 1;
// match to be turned into link
matches.forEach(match => {
pageNumber = match.match(/\d+/)[0];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes citations are present but not clickable when asking for the document summary. When I highlighting text and get the summary there are no citations and also when I ask a question in the chat about the document there are no citations

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Mohammed-AbdulRahman-Apryse review to make sure we have citations that are clickable, add citations on selected text, on summary link clicks, and free-text query from input.

Copy link
Collaborator Author

@Mohammed-AbdulRahman-Apryse Mohammed-AbdulRahman-Apryse Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The citation is clickable when the current viewable page is different than the citation page number. When clicking the citation and the webviewer is showing the same page number, it has no effect.

Concerning highlighting text and get the summary there are no citations, this has been fixed in Mohammed-AbdulRahman-Apryse@09dbab4.

A video is attached to the end of this review showing this.

if (pageNumber > 0 &&
pageNumber <= window.WebViewer.getInstance().Core.documentViewer.getDocument().getPageCount()) {
const pageLink = `<a href="#" style="color:blue;" onclick="window.WebViewer.getInstance().Core.documentViewer.setCurrentPage(${pageNumber}, true);">[${pageNumber}]</a>`;
text = text.replaceAll(match, `${pageLink}`);
}
});
}

return text;
}

// Helper to separate grouped citations on form [1, 2, 3] or [1-3] into individual [1][2][3]
separateGroupedCitations = (text, pattern) => {
let matches = text.match(pattern);
if (matches && matches.length > 0) {
let formattedMatchNumbers = '';
matches.forEach(match => {
let matchNumbers = match.match(/\d+/g);
matchNumbers.forEach(matchNumber => {
formattedMatchNumbers += `[${matchNumber}]`;
});

text = text.replaceAll(match, formattedMatchNumbers);
formattedMatchNumbers = '';
});
}

return text;
}

clearHistory() {
this.conversationHistory = [];
}
}

// Export for use in other modules
export default function createChatbot() {
return new ChatbotClient();
}
Loading