-
Notifications
You must be signed in to change notification settings - Fork 14
Adding webviewer-ask-ai sample #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Mohammed-AbdulRahman-Apryse
wants to merge
3
commits into
ApryseSDK:main
Choose a base branch
from
Mohammed-AbdulRahman-Apryse:Mohammed-webviewer-samples
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]; | ||
| 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(); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.