From f4689365405f9a0894fc38744799f1e02d89cb4a Mon Sep 17 00:00:00 2001 From: Mohammed AbdulRahman Date: Wed, 3 Dec 2025 13:10:24 +0300 Subject: [PATCH 1/3] Adding webviewer-ask-ai A brand-new sample related to https://apryse.atlassian.net/browse/AS-196 --- webviewer-ask-ai/.env.example | 10 + webviewer-ask-ai/.gitignore | 7 + webviewer-ask-ai/LICENSE | 2 + webviewer-ask-ai/README.md | 45 + webviewer-ask-ai/client/assets/favicon.svg | 13 + webviewer-ask-ai/client/chatbot.js | 173 ++ webviewer-ask-ai/client/config/ui/custom.json | 1165 +++++++++++++ .../client/config/ui/functionMap.js | 199 +++ webviewer-ask-ai/client/config/ui/styles.css | 79 + webviewer-ask-ai/client/globals.js | 7 + webviewer-ask-ai/client/index.html | 12 + webviewer-ask-ai/client/index.js | 55 + webviewer-ask-ai/mainsamplesource.json | 45 + webviewer-ask-ai/package-lock.json | 1486 +++++++++++++++++ webviewer-ask-ai/package.json | 25 + webviewer-ask-ai/server/handler.js | 204 +++ webviewer-ask-ai/server/serve.js | 28 + .../tools/copy-webviewer-files.js | 15 + 18 files changed, 3570 insertions(+) create mode 100644 webviewer-ask-ai/.env.example create mode 100644 webviewer-ask-ai/.gitignore create mode 100644 webviewer-ask-ai/LICENSE create mode 100644 webviewer-ask-ai/README.md create mode 100644 webviewer-ask-ai/client/assets/favicon.svg create mode 100644 webviewer-ask-ai/client/chatbot.js create mode 100644 webviewer-ask-ai/client/config/ui/custom.json create mode 100644 webviewer-ask-ai/client/config/ui/functionMap.js create mode 100644 webviewer-ask-ai/client/config/ui/styles.css create mode 100644 webviewer-ask-ai/client/globals.js create mode 100644 webviewer-ask-ai/client/index.html create mode 100644 webviewer-ask-ai/client/index.js create mode 100644 webviewer-ask-ai/mainsamplesource.json create mode 100644 webviewer-ask-ai/package-lock.json create mode 100644 webviewer-ask-ai/package.json create mode 100644 webviewer-ask-ai/server/handler.js create mode 100644 webviewer-ask-ai/server/serve.js create mode 100644 webviewer-ask-ai/tools/copy-webviewer-files.js diff --git a/webviewer-ask-ai/.env.example b/webviewer-ask-ai/.env.example new file mode 100644 index 00000000..92fe747c --- /dev/null +++ b/webviewer-ask-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_TEMPERATURE=your-openai-temperature-here +OPENAI_MAX_TOKENS=your-openai-max-tokens-here + +# Server Configuration +PORT=4040 \ No newline at end of file diff --git a/webviewer-ask-ai/.gitignore b/webviewer-ask-ai/.gitignore new file mode 100644 index 00000000..8bc4ae21 --- /dev/null +++ b/webviewer-ask-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-ask-ai/LICENSE b/webviewer-ask-ai/LICENSE new file mode 100644 index 00000000..81ffa45f --- /dev/null +++ b/webviewer-ask-ai/LICENSE @@ -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). \ No newline at end of file diff --git a/webviewer-ask-ai/README.md b/webviewer-ask-ai/README.md new file mode 100644 index 00000000..1fd656aa --- /dev/null +++ b/webviewer-ask-ai/README.md @@ -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. \ No newline at end of file diff --git a/webviewer-ask-ai/client/assets/favicon.svg b/webviewer-ask-ai/client/assets/favicon.svg new file mode 100644 index 00000000..65864609 --- /dev/null +++ b/webviewer-ask-ai/client/assets/favicon.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/webviewer-ask-ai/client/chatbot.js b/webviewer-ask-ai/client/chatbot.js new file mode 100644 index 00000000..c3a0df2b --- /dev/null +++ b/webviewer-ask-ai/client/chatbot.js @@ -0,0 +1,173 @@ +// Browser-compatible chatbot client +class ChatbotClient { + + // 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) { + try { + const response = await fetch('/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: message, + promptType: promptLine + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + 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 ${i}\n${text}`; + loadedPages++; + + // When all pages are loaded, combine and send + if (loadedPages === pageCount) { + const completeText = pageTexts.join('\n\n'); + + this.sendMessage(promptType, completeText).then(response => { + let responseText = this.responseText(response); + responseText = this.formatText(promptType, responseText); + createBubble(responseText, 'system'); + }).catch(error => { + createBubble(`Error: ${error.message}`, 'system'); + }); + } + } catch (error) { + pageTexts[i - 1] = `<> 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).then(response => { + let responseText = this.responseText(response); + responseText = this.formatText(promptType, responseText); + createBubble(responseText, 'system'); + }).catch(error => { + createBubble(`Error: ${error.message}`, 'system'); + }); + } + } + } + }); + }; + + // 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) => { + let matches = text.match(/\[\d+(?:\s*,\s*\d+)+\]/g); + 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 = ''; + }); + } + + switch (promptType) { + case 'DOCUMENT_SUMMARY': + case 'SELECTED_TEXT_SUMMARY': + case 'DOCUMENT_QUESTION': + matches = text.match(/\[\d+\][.]/g); + break; + case 'DOCUMENT_KEYWORDS': + matches = text.match(/\[\d+\]/g); + break; + default: + break; + } + + 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 = `[${pageNumber}]`; + if (promptType === 'DOCUMENT_KEYWORDS') + text = text.replaceAll(match, `${pageLink}`); + else + text = text.replaceAll(match, `${pageLink}.
`); + } + }); + } + + if (promptType === 'DOCUMENT_KEYWORDS') { + let lines = text.split(/•\s*/).filter(Boolean); + text = lines.map(line => `• ${line.trim()}`).join('
'); + } + + return text; + } +} + +// Export for use in other modules +export default function createChatbot() { + return new ChatbotClient(); +} \ No newline at end of file diff --git a/webviewer-ask-ai/client/config/ui/custom.json b/webviewer-ask-ai/client/config/ui/custom.json new file mode 100644 index 00000000..0dd251d2 --- /dev/null +++ b/webviewer-ask-ai/client/config/ui/custom.json @@ -0,0 +1,1165 @@ +{ + "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", + "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": true, + "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 + }, + "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": [ + "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" + }, + "askWebSDKPanelToggle": { + "dataElement": "askWebSDKPanelToggle", + "title": "{{APP_SITE_NAME}}", + "type": "toggleButton", + "img": "{{ASK_WEB_SDK_ICO}}", + "toggleElement": "askWebSDKPanel" + }, + "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", + "askWebSDKPanelToggle" + ] + }, + "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" + }, + "askWebSDKPanel": { + "dataElement": "askWebSDKPanel", + "render": "askWebSDKPanelRender", + "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": "askWebSDKButton", + "type": "customButton", + "img": "{{ASK_WEB_SDK_ICO}}", + "title": "Summarize Selection", + "onClick": "askWebSDKPopupClick" + }, + { + "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" + } + ] + } +} \ No newline at end of file diff --git a/webviewer-ask-ai/client/config/ui/functionMap.js b/webviewer-ask-ai/client/config/ui/functionMap.js new file mode 100644 index 00000000..d762a961 --- /dev/null +++ b/webviewer-ask-ai/client/config/ui/functionMap.js @@ -0,0 +1,199 @@ +import { Spinner } from './spinjs/spin.js'; +const spinOptions = { + lines: 13, // The number of lines to draw + length: 38, // The length of each line + width: 17, // The line thickness + radius: 45, // The radius of the inner circle + scale: 1, // Scales overall size of the spinner + corners: 1, // Corner roundness (0..1) + speed: 1, // Rounds per second + rotate: 0, // The rotation offset + animation: 'spinner-line-fade-quick', // The CSS animation name for the lines + direction: 1, // 1: clockwise, -1: counterclockwise + color: '#ffffff', // CSS color or array of colors + fadeColor: 'transparent', // CSS color or array of colors + top: '50%', // Top position relative to parent + left: '50%', // Left position relative to parent + shadow: '0 0 1px transparent', // Box-shadow for the lines + zIndex: 2000000000, // The z-index (defaults to 2e9) + className: 'spinner', // The CSS class to assign to the spinner + position: 'absolute', // Element positioning +}; +let askWebSDKMainDiv = null; +let askWebSDKChattingDiv = null; +const systemMessages = [ + { + type: 'welcoming', + content: `Hello, I'm ${APP_SITE_NAME}. How can I help you?`, + }, + { + type: 'info', + content: `Select the options below to get started. You can also select text in the document and click the popup to summarize the selected text.`, + }, + { + type: 'info', + content: [ + { + id: 0, + type: 'info', + content: 'Choose:', + promptType: '', + }, + { + id: 1, + type: 'question', + content: 'Summarize Document', + promptType: 'DOCUMENT_SUMMARY' + }, + { + id: 2, + type: 'question', + content: 'List Keywords', + promptType: 'DOCUMENT_KEYWORDS' + } + ] + } +]; + +const functionMap = { + // Render the WebViewer chat panel + 'askWebSDKPanelRender': () => { + // The main container div + askWebSDKMainDiv = document.createElement('div'); + askWebSDKMainDiv.id = 'askWebSDKMainDiv'; + askWebSDKMainDiv.className = 'askWebSDKMainDivClass'; + + // Header container div with document title + let askWebSDKHeaderDiv = document.createElement('div'); + askWebSDKHeaderDiv.id = 'askWebSDKHeaderDiv'; + askWebSDKHeaderDiv.className = 'askWebSDKHeaderDivClass'; + + let askWebSDKHeaderTitle = document.createElement('h4'); + askWebSDKHeaderTitle.id = 'askWebSDKHeaderTitle'; + askWebSDKHeaderTitle.className = 'askWebSDKHeaderTitleClass'; + askWebSDKHeaderTitle.innerText = `Document: ${window.WebViewer.getInstance().Core.documentViewer.getDocument().getFilename()}`; + askWebSDKHeaderDiv.appendChild(askWebSDKHeaderTitle); + askWebSDKMainDiv.appendChild(askWebSDKHeaderDiv); + + // Chatting container div with system and user messages + askWebSDKChattingDiv = document.createElement('div'); + askWebSDKChattingDiv.id = 'askWebSDKChattingDiv'; + askWebSDKChattingDiv.className = 'askWebSDKChattingDivClass'; + systemMessages.forEach((message) => { + let messageDiv = document.createElement('div'); + messageDiv.className = 'askWebSDKSystemMessageClass'; + if (Array.isArray(message.content)) { + message.content.forEach((contentItem) => { + let systemContentDiv = (contentItem.type === 'info') ? document.createElement('div') : document.createElement('li'); + systemContentDiv.className = (contentItem.type === 'info') ? 'askWebSDKInfoMessageClass' : 'askWebSDKQuestionMessageClass'; + if (contentItem.type === 'question') { + systemContentDiv.onmouseover = () => { + systemContentDiv.style.textDecoration = 'underline'; + systemContentDiv.style.color = 'blue'; + }; + systemContentDiv.onmouseout = () => { + systemContentDiv.style.textDecoration = 'none'; + systemContentDiv.style.color = 'black'; + }; + systemContentDiv.onclick = () => { + createBubble(contentItem.content, 'user'); + const spinner = new Spinner(spinOptions); + spinner.spin(askWebSDKMainDiv); + + // Create a wrapper callback that stops the spinner after createBubble is called + const callbackWrapper = (...args) => { + createBubble(...args); + spinner.stop(); + }; + + window.chatbot.getAllText(contentItem.promptType, callbackWrapper); + }; + } + systemContentDiv.innerText = contentItem.content; + messageDiv.appendChild(systemContentDiv); + }); + } else + messageDiv.innerText = message.content; + + askWebSDKChattingDiv.appendChild(messageDiv); + }); + + askWebSDKMainDiv.appendChild(askWebSDKChattingDiv); + + // Question input container div with input box and send button + let askWebSDKQuestionDiv = document.createElement('div'); + askWebSDKQuestionDiv.id = 'askWebSDKQuestionDiv'; + askWebSDKQuestionDiv.className = 'askWebSDKQuestionDivClass'; + + let askWebSDKQuestionInput = document.createElement('input'); + askWebSDKQuestionInput.id = 'askWebSDKQuestionInput'; + askWebSDKQuestionInput.className = 'askWebSDKQuestionInputClass'; + askWebSDKQuestionInput.type = 'text'; + askWebSDKQuestionInput.placeholder = 'Ask your question here...'; + askWebSDKQuestionInput.onkeydown = (event) => { + if (event.key === 'Enter') + askWebSDKQuestionButton.click(); + }; + + let askWebSDKQuestionButton = document.createElement('button'); + askWebSDKQuestionButton.id = 'askWebSDKQuestionButton'; + askWebSDKQuestionButton.className = 'askWebSDKQuestionButtonClass'; + askWebSDKQuestionButton.innerText = 'Send'; + askWebSDKQuestionButton.onclick = () => { + if (askWebSDKQuestionInput.value.trim() !== '') { + createBubble(askWebSDKQuestionInput.value.trim(), 'user'); + + // Start loading spinner on main div to block all interactions + const spinner = new Spinner(spinOptions); + spinner.spin(askWebSDKMainDiv); + + // Send question as document query + window.chatbot.sendMessage('DOCUMENT_QUESTION', askWebSDKQuestionInput.value.trim()).then(response => { + spinner.stop(); + let responseText = window.chatbot.responseText(response); + responseText = window.chatbot.formatText('DOCUMENT_QUESTION', responseText); + createBubble(responseText, 'system'); + }).catch(error => { + spinner.stop(); + createBubble(`Error: ${error.message}`, 'system'); + }); + } + }; + + askWebSDKQuestionDiv.appendChild(askWebSDKQuestionInput); + askWebSDKQuestionDiv.appendChild(askWebSDKQuestionButton); + askWebSDKMainDiv.appendChild(askWebSDKQuestionDiv); + + return askWebSDKMainDiv; + }, + // Handle selected text summary popup click + 'askWebSDKPopupClick': () => { + createBubble('Summarize the selected text.', 'user'); + + // Start loading spinner for selected text summary on main div + const spinner = new Spinner(spinOptions); + spinner.spin(askWebSDKMainDiv); + + //Combine into single container for all bubble responses + window.chatbot.sendMessage('SELECTED_TEXT_SUMMARY', window.selectedText).then(response => { + spinner.stop(); + let responseText = window.chatbot.responseText(response); + responseText = window.chatbot.formatText('SELECTED_TEXT_SUMMARY', responseText); + createBubble(responseText, 'system'); + }).catch(error => { + spinner.stop(); + createBubble(`Error: ${error.message}`, 'system'); + }); + }, +}; + +// Function to create a chat bubble +function createBubble(content, role) { + let messageDiv = document.createElement('div'); + messageDiv.className = (role === 'system') ? 'askWebSDKSystemMessageClass' : 'askWebSDKUserMessageClass'; + messageDiv.innerHTML = content; + askWebSDKChattingDiv.appendChild(messageDiv); + askWebSDKChattingDiv.scrollTop = askWebSDKChattingDiv.scrollHeight; +} + +export default functionMap; \ No newline at end of file diff --git a/webviewer-ask-ai/client/config/ui/styles.css b/webviewer-ask-ai/client/config/ui/styles.css new file mode 100644 index 00000000..72721af3 --- /dev/null +++ b/webviewer-ask-ai/client/config/ui/styles.css @@ -0,0 +1,79 @@ +.askWebSDKMainDiv { + display: flex; + flex-direction: column; + overflow: hidden; + position: relative; +} + +.askWebSDKHeaderDivClass { + background-color: #34495e; + color: white; + display: flex; + align-items: center; + padding-left: 10px; +} + +.askWebSDKChattingDivClass { + display: flex; + flex-direction: column; + flex: 1 1 auto; + overflow: auto; + padding: 10px 10px 150px 10px; + height: 80vh; + position: fixed; +} + +.askWebSDKQuestionDivClass { + width: 600px; + background-color: #ecf0f1; + padding: 10px; + box-sizing: border-box; + display: flex; + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 1000; + align-items: center; + justify-self: end; +} + +.askWebSDKQuestionInputClass { + width: 85%; + margin-right: 5px; +} + +.askWebSDKQuestionButtonClass { + background-color: Blue; + color: white; + cursor: pointer; +} + +.askWebSDKSystemMessageClass { + background-color: #ecf0f1; + color: black; + align-self: flex-start; + margin: 5px; + padding: 10px; + border-radius: 5px; + max-width: 70%; +} + +.askWebSDKUserMessageClass { + background-color: #3498db; + color: white; + align-self: flex-end; + margin: 5px; + padding: 10px; + border-radius: 5px; + max-width: 70%; +} + +.askWebSDKInfoMessageClass { + margin-top: 5px; +} + +.askWebSDKQuestionMessageClass { + margin-top: 5px; + cursor: pointer; +} \ No newline at end of file diff --git a/webviewer-ask-ai/client/globals.js b/webviewer-ask-ai/client/globals.js new file mode 100644 index 00000000..78365859 --- /dev/null +++ b/webviewer-ask-ai/client/globals.js @@ -0,0 +1,7 @@ +// Globals for the app +const APP_SITE_NAME = "WebViewer Ask AI"; +const ASK_WEB_SDK_ICO = ""; + +const setSelectedText = (selectedText) => { + window.selectedText = selectedText; +} \ No newline at end of file diff --git a/webviewer-ask-ai/client/index.html b/webviewer-ask-ai/client/index.html new file mode 100644 index 00000000..828322cc --- /dev/null +++ b/webviewer-ask-ai/client/index.html @@ -0,0 +1,12 @@ + + + + + + + + +
+ + + \ No newline at end of file diff --git a/webviewer-ask-ai/client/index.js b/webviewer-ask-ai/client/index.js new file mode 100644 index 00000000..e462b17a --- /dev/null +++ b/webviewer-ask-ai/client/index.js @@ -0,0 +1,55 @@ +import createChatbot from './chatbot.js'; +import functionMap from './config/ui/functionMap.js'; +const customUIFile = './config/ui/custom.json'; + +WebViewer({ + path: 'lib', + initialDoc: 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/Report_2011.pdf', + fullAPI: true, + loadAsPDF: true, + enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File + css: 'config/ui/styles.css', + licenseKey: 'YOUR_LICENSE_KEY', +}, document.getElementById('viewer') +).then(instance => { + + // Import modular components configuration from JSON file + importModularComponents(instance); + + // Listen for text selection events + // The user can select text in the document, to be added as context for the chatbot to be processed + instance.Core.documentViewer.addEventListener('textSelected', (quads, selectedText) => { + if (selectedText) { + // Set selected text as context for chatbot + setSelectedText(selectedText); + } + }); + + // Listen for document loaded event to initialize the chatbot panel + instance.Core.documentViewer.addEventListener('documentLoaded', () => { + instance.UI.closeElements(['askWebSDKPanel']); + instance.UI.openElements(['askWebSDKPanel']); + instance.UI.setPanelWidth('askWebSDKPanel', 600); + + // Initialize chatbot + const chatbot = createChatbot(); + window.chatbot = chatbot; + }); +}); + +// Import modular components configuration from JSON file +const importModularComponents = async (instance) => { + 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()); + customUIConfig = customUIConfig.replaceAll("{{APP_SITE_NAME}}", APP_SITE_NAME); + customUIConfig = customUIConfig.replaceAll("{{ASK_WEB_SDK_ICO}}", ASK_WEB_SDK_ICO); + + await instance.UI.importModularComponents(JSON.parse(customUIConfig), functionMap); + } catch (error) { + throw new Error(`Failed to import modular components configuration: ${error.message}`); + } +}; \ No newline at end of file diff --git a/webviewer-ask-ai/mainsamplesource.json b/webviewer-ask-ai/mainsamplesource.json new file mode 100644 index 00000000..be4ea06e --- /dev/null +++ b/webviewer-ask-ai/mainsamplesource.json @@ -0,0 +1,45 @@ +{ + "githubId": "", + "files": [ + { + "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/client/index.js", + "description": "", + "reason": "" + }, + { + "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/client/chatbot.js", + "description": "", + "reason": "" + }, + { + "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/client/globals.js", + "description": "", + "reason": "" + }, + { + "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/server/handler.js", + "description": "", + "reason": "" + }, + { + "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/server/server.js", + "description": "", + "reason": "" + }, + { + "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/client/config/ui/custom.json", + "description": "", + "reason": "" + }, + { + "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/client/config/ui/functionMap.js", + "description": "", + "reason": "" + }, + { + "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/client/config/ui/styles.css", + "description": "", + "reason": "" + } + ] +} diff --git a/webviewer-ask-ai/package-lock.json b/webviewer-ask-ai/package-lock.json new file mode 100644 index 00000000..fac46bc9 --- /dev/null +++ b/webviewer-ask-ai/package-lock.json @@ -0,0 +1,1486 @@ +{ + "name": "webviewer-ask-ai", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webviewer-ask-ai", + "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "@langchain/core": "^1.0.6", + "@langchain/openai": "^1.1.2", + "@pdftron/webviewer": "^11.9.0", + "dotenv": "^17.2.3", + "spin.js": "^4.1.2" + }, + "devDependencies": { + "body-parser": "^2.2.0", + "express": "^5.1.0", + "fs-extra": "^11.3.2", + "open": "^10.2.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.1", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.1.tgz", + "integrity": "sha512-vdUoj2CVbb+0Qszi8llP34vdUCfP7bfA9VoFr4Se1pFGu7VAPnk8lBnRat9IvqSxMfTvOHJSd7Rn6TUPjzKsnA==", + "license": "MIT", + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.64", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "^7.0.0", + "uuid": "^10.0.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@langchain/openai": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.1.3.tgz", + "integrity": "sha512-p+xR+4HRms5Ozjf5miC6U2AYRyNVSTdO7AMBkMYs1Tp6DWHBd+mQ72H8Ogd2dKrPuS5UDJ5dbpI1fS+OrTbgQQ==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^6.9.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.0.0" + } + }, + "node_modules/@pdftron/webviewer": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-11.9.0.tgz", + "integrity": "sha512-cW1XFG2hLQ7hUFtI+8HuyrmHc2+fN6t/9UQuaJQ/6Ct09qXDS7ljpvRYNJ1YyYAeyqgOo8Q1ZzZ/1zdv+4l25g==" + }, + "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==", + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "dev": true, + "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.0", + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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-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-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "license": "MIT", + "engines": { + "node": ">=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==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "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.3.82", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.82.tgz", + "integrity": "sha512-RTcxtRm0zp2lV+pMesMW7EZSsIlqN7OmR2F6sZ/sOFQwmcLVl+VErMPV4VkX4Sycs4/EIAFT5hpr36EqiHoikQ==", + "license": "MIT", + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.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": "*" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.9.1.tgz", + "integrity": "sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg==", + "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-retry": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.0.tgz", + "integrity": "sha512-xL4PiFRQa/f9L9ZvR4/gUCRNus4N8YX80ku8kv9Jqz+ZokkiZLM0bcvX0gm1F3PDi9SPRsww1BDsTWgE6Y1GLQ==", + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/spin.js": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/spin.js/-/spin.js-4.1.2.tgz", + "integrity": "sha512-ua/yEpxEwyEUWs57tMQYdik/KJ12sQRyMXjSlK/Ai927aEUDVY3FXUi4ml4VvlLCTQNIjC6tHyjSLBrJzFAqMA==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/webviewer-ask-ai/package.json b/webviewer-ask-ai/package.json new file mode 100644 index 00000000..b0bee753 --- /dev/null +++ b/webviewer-ask-ai/package.json @@ -0,0 +1,25 @@ +{ + "name": "webviewer-ask-ai", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "postinstall": "node tools/copy-webviewer-files.js", + "start": "node server/serve.js" + }, + "author": "Apryse Systems Inc.", + "devDependencies": { + "body-parser": "^2.2.0", + "express": "^5.1.0", + "fs-extra": "^11.3.2", + "open": "^10.2.0" + }, + "dependencies": { + "@langchain/core": "^1.0.6", + "@langchain/openai": "^1.1.2", + "@pdftron/webviewer": "^11.9.0", + "dotenv": "^17.2.3", + "spin.js": "^4.1.2" + } +} diff --git a/webviewer-ask-ai/server/handler.js b/webviewer-ask-ai/server/handler.js new file mode 100644 index 00000000..59f106e9 --- /dev/null +++ b/webviewer-ask-ai/server/handler.js @@ -0,0 +1,204 @@ +import { ChatOpenAI } from '@langchain/openai'; +import { HumanMessage, SystemMessage } from '@langchain/core/messages'; +import { StringOutputParser } from '@langchain/core/output_parsers'; +import dotenv from 'dotenv'; + +dotenv.config(); + +// Guard rails configuration for different prompt types +const PROMPT_GUARD_RAILS = { + 'DOCUMENT_SUMMARY': { + systemPrompt: 'You are a document summarizer specializing in PDF documents. Summarize the provided text concisely under 300 words. CRITICALLY IMPORTANT: For each sentence in your summary, you MUST add square brackets with the page number where that information came from (e.g., [1] for page 1, [2] for page 2, etc.). The document text is divided by page break markers in the format "<> Page N" where N is the page number. When you see "<> Page 3", all content following that marker until the next page break is from page 3. Always cite the correct page number for each fact or statement. Example: "The company reported strong earnings [1]. The new policy takes effect in January [2]."', + maxTokens: 500, + temperature: 0.3 + }, + 'DOCUMENT_KEYWORDS': { + systemPrompt: 'You are a keyword extraction specialist. Create a bulleted list of the 10 most important keywords from the provided document text. CRITICALLY IMPORTANT: For each keyword, you MUST include the page number where it appears in square brackets. The document text is divided by page break markers in the format "<> Page N" where N is the page number. When you see "<> Page 3", all content following that marker until the next page break is from page 3. Format each keyword as: "• Keyword [page#]" Example: "• Federal Acquisition Regulation [1]" or "• Section 508 compliance [2]". Always cite the correct page number for each keyword.', + maxTokens: 500, + temperature: 0.1 + }, + 'SELECTED_TEXT_SUMMARY': { + systemPrompt: 'You are a text summarizer for selected content. Provide a concise summary of the selected text, highlighting the main points and key information. Be clear and focused.', + maxTokens: 500, + temperature: 0.3 + }, + 'DOCUMENT_QUESTION': { + systemPrompt: 'You are a document Q&A assistant. Answer questions about the provided document content accurately and concisely. Use specific information from the document to support your answers. If you cannot find relevant information in the document, say so clearly. CRITICALLY IMPORTANT: For each statement or fact in your answer, you MUST add square brackets with the page number where that information came from (e.g., [1] for page 1, [2] for page 2, etc.). The document text is divided by page break markers in the format "<> Page N" where N is the page number. When you see "<> Page 3", all content following that marker until the next page break is from page 3. Always cite the correct page number for each fact or statement. Example: "The policy requires annual reviews [3]. Training must be completed within 30 days [5]."', + maxTokens: 500, + temperature: 0.4 + }, + 'default': { + systemPrompt: 'You are a helpful assistant that helps users with PDF documents and general questions. Be concise and helpful.', + maxTokens: 1000, + temperature: 0.7 + } +}; + +// Function to estimate tokens (rough approximation: 1 token ≈ 4 characters) +function estimateTokens(text) { + return Math.ceil(text.length / 4); +} + +// Function to chunk text to fit within token limits +function chunkText(text, maxTokens = 12000) { // Leave room for system prompt and response + const words = text.split(' '); + const chunks = []; + let currentChunk = ''; + + for (const word of words) { + const testChunk = currentChunk + (currentChunk ? ' ' : '') + word; + if (estimateTokens(testChunk) > maxTokens && currentChunk) { + chunks.push(currentChunk); + currentChunk = word; + } else { + currentChunk = testChunk; + } + } + + if (currentChunk) { + chunks.push(currentChunk); + } + + return chunks; +} + +// Function to get system prompt and settings based on prompt type +function getPromptSettings(promptType) { + return PROMPT_GUARD_RAILS[promptType] || PROMPT_GUARD_RAILS['default']; +} + +// Initialize the OpenAI chat llm and parser +let llm = null; +let parser = null; + +// Initialize LangChain components +function initializeLangChain() { + if (!process.env.OPENAI_API_KEY) { + console.error('❌ Missing OPENAI_API_KEY in .env file'); + return false; + } + + try { + llm = new ChatOpenAI({ + apiKey: process.env.OPENAI_API_KEY, + modelName: process.env.OPENAI_MODEL || 'gpt-3.5-turbo', + temperature: parseFloat(process.env.OPENAI_TEMPERATURE) || 0.7, + maxTokens: parseInt(process.env.OPENAI_MAX_TOKENS) || 1000, + }); + + parser = new StringOutputParser(); + console.log('✅ LangChain initialized successfully'); + return true; + } catch (error) { + console.error('❌ Error initializing LangChain:', error); + return false; + } +} + +export default (app) => { + + // Initialize LangChain on startup + const langChainReady = initializeLangChain(); + + // Chat API endpoint + app.post('/api/chat', async (request, response) => { + try { + if (!langChainReady || !llm) { + return response.status(500).json({ + error: 'Chat service not available. Please check server configuration.' + }); + } + + const { message, promptType } = request.body; + + if (!message || typeof message !== 'string') { + return response.status(400).json({ error: 'Message is required' }); + } + + // Get appropriate prompt settings based on prompt type + const promptSettings = getPromptSettings(promptType); + + // Check if message is too long and handle accordingly + const estimatedTokens = estimateTokens(message) + estimateTokens(promptSettings.systemPrompt) + 500; // Buffer for history and response + + let finalContent; + + if (estimatedTokens > 16000) { // Leave buffer for llm limit + + // For keyword extraction, we can chunk the document and extract keywords from each chunk + if (promptType?.includes('keywords') || promptType?.includes('Keywords')) { + const chunks = chunkText(message, 12000); + + const allKeywords = []; + + for (let i = 0; i < chunks.length; i++) { + const chunkMessages = [ + new SystemMessage(`${promptSettings.systemPrompt} This is chunk ${i + 1} of ${chunks.length}. Extract keywords from this section only.`), + new HumanMessage(chunks[i]) + ]; + + // update llm settings for this prompt + llm.temperature = promptSettings.temperature; + llm.maxTokens = 150; + + const chunkResponse = await llm.invoke(chunkMessages); + const chunkContent = await parser.parse(chunkResponse); + allKeywords.push(chunkContent); + } + + // Now consolidate all keywords into final list + const consolidationMessages = [ + new SystemMessage('You are a keyword consolidation specialist. From the following keyword lists extracted from different sections of a document, create a final bulleted list of the 10 most important and representative keywords. Remove duplicates and prioritize the most significant terms.'), + new HumanMessage(`Consolidate these keyword lists into the top 10 keywords:\n\n${allKeywords.join('\n\n---\n\n')}`) + ]; + + // update llm settings for consolidation + llm.temperature = 0.1; + llm.maxTokens = 200; + + const finalResponse = await llm.invoke(consolidationMessages); + finalContent = await parser.parse(finalResponse); + + } else { + // For other prompt types, use first chunk with warning + const chunks = chunkText(message, 12000); + const messages = [ + new SystemMessage(`${promptSettings.systemPrompt}\n\nNOTE: This document was too long, so only the first section is being processed.`), + new HumanMessage(chunks[0]) + ]; + + // update llm settings for this prompt + llm.temperature = promptSettings.temperature; + llm.maxTokens = promptSettings.maxTokens; + + const response_data = await llm.invoke(messages); + finalContent = await parser.parse(response_data); + } + + } else { + // Normal processing for documents within token limits + const messages = [ + new SystemMessage(promptSettings.systemPrompt), + new HumanMessage(message) + ]; + + // update llm with prompt-specific settings + llm.temperature = promptSettings.temperature; + llm.maxTokens = promptSettings.maxTokens; + + const response_data = await llm.invoke(messages); + finalContent = await parser.parse(response_data); + } + + // Ensure sending a clean string response + const cleanResponse = typeof finalContent === 'string' ? finalContent : + (finalContent?.content || JSON.stringify(finalContent)); + + response.status(200).json({ response: cleanResponse }); + } catch (error) { + response.status(500).json({ + error: 'An error occurred while processing your request' + }); + } + }); +} diff --git a/webviewer-ask-ai/server/serve.js b/webviewer-ask-ai/server/serve.js new file mode 100644 index 00000000..f10708a7 --- /dev/null +++ b/webviewer-ask-ai/server/serve.js @@ -0,0 +1,28 @@ +// 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 app = express(); + +// Use JSON body parser for API endpoints +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use('/client', express.static('client')); // For statically serving 'client' folder at '/' + +handler(app); + +// Run server +app.listen(process.env.PORT, 'localhost', (err) => { + if (err) { + console.error(err); + } else { + console.info(`Server is listening at http://localhost:${process.env.PORT}/client/index.html`); + open(`http://localhost:${process.env.PORT}/client/index.html`); + } +}); \ No newline at end of file diff --git a/webviewer-ask-ai/tools/copy-webviewer-files.js b/webviewer-ask-ai/tools/copy-webviewer-files.js new file mode 100644 index 00000000..5296e1e3 --- /dev/null +++ b/webviewer-ask-ai/tools/copy-webviewer-files.js @@ -0,0 +1,15 @@ +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'); + await fs.copy('./node_modules/spin.js/spin.js', './client/config/ui/spinjs/spin.js'); + await fs.copy('./node_modules/spin.js/spin.css', './client/config/ui/spinjs/spin.css'); + console.log('WebViewer files copied over successfully'); + } catch (err) { + console.error(err); + } +}; + +copyFiles(); From 09dbab4552395a03a296592960013b8ec1b73510 Mon Sep 17 00:00:00 2001 From: Mohammed AbdulRahman Date: Mon, 8 Dec 2025 21:15:38 +0300 Subject: [PATCH 2/3] Adding chat history to webviewer-ask-ai sample - The chat history is added in: A. client\chatbot.js B. server\handler.js - Refactoring the page citation links in: client\chatbot.js - Maintaining the conversation sequence in: A. client\config\ui\functionMap.js B. client\globals.js - Option to summarize when clicking that button or ask questions directly about the selection in: client\config\ui\functionMap.js - Maintaining Artificial Intellegance terms by changing "System" into "Assistant" and "User" into "Human" in: A. client\chatbot.js B. client\config\ui\functionMap.js C. client\config\ui\styles.css D. server\handler.js - Added githubId value in mainsamplesource.json - Added sample name to lerna.js - Added the sample name to the generic webviewer-samples readme --- README.md | 1 + lerna.json | 1 + webviewer-ask-ai/client/chatbot.js | 95 +++++++---- .../client/config/ui/functionMap.js | 159 ++++++++++++------ webviewer-ask-ai/client/config/ui/styles.css | 4 +- webviewer-ask-ai/client/globals.js | 5 +- webviewer-ask-ai/client/index.html | 2 +- webviewer-ask-ai/client/index.js | 13 +- webviewer-ask-ai/mainsamplesource.json | 2 +- webviewer-ask-ai/package-lock.json | 2 +- webviewer-ask-ai/package.json | 2 +- webviewer-ask-ai/server/handler.js | 40 +++-- 12 files changed, 212 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index e534814e..4a59eed6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lerna.json b/lerna.json index 4c640e78..d313e365 100644 --- a/lerna.json +++ b/lerna.json @@ -8,6 +8,7 @@ "webviewer-annotations-nodejs", "webviewer-annotations-php", "webviewer-annotations-sqlite3", + "webviewer-ask-ai", "webviewer-barcode", "webviewer-blazor", "webviewer-blazor-wasm", diff --git a/webviewer-ask-ai/client/chatbot.js b/webviewer-ask-ai/client/chatbot.js index c3a0df2b..bdd15a08 100644 --- a/webviewer-ask-ai/client/chatbot.js +++ b/webviewer-ask-ai/client/chatbot.js @@ -1,15 +1,19 @@ // 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) { + 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: { @@ -17,7 +21,8 @@ class ChatbotClient { }, body: JSON.stringify({ message: message, - promptType: promptLine + promptType: promptLine, + history: historyToSend }) }); @@ -27,6 +32,14 @@ class ChatbotClient { 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; @@ -54,12 +67,16 @@ class ChatbotClient { if (loadedPages === pageCount) { const completeText = pageTexts.join('\n\n'); - this.sendMessage(promptType, completeText).then(response => { + // 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, 'system'); + createBubble(responseText, 'assistant'); }).catch(error => { - createBubble(`Error: ${error.message}`, 'system'); + createBubble(`Error: ${error.message}`, 'assistant'); }); } } catch (error) { @@ -70,12 +87,15 @@ class ChatbotClient { if (loadedPages === pageCount) { const completeText = pageTexts.join('\n\n'); - this.sendMessage(promptType, completeText).then(response => { + this.sendMessage(promptType, completeText, { + useEmptyHistory: true, + skipHistoryUpdate: false + }).then(response => { let responseText = this.responseText(response); responseText = this.formatText(promptType, responseText); - createBubble(responseText, 'system'); + createBubble(responseText, 'assistant'); }).catch(error => { - createBubble(`Error: ${error.message}`, 'system'); + createBubble(`Error: ${error.message}`, 'assistant'); }); } } @@ -112,33 +132,29 @@ class ChatbotClient { // Format text to include cited page links // and page breaks based on prompt type formatText = (promptType, text) => { - let matches = text.match(/\[\d+(?:\s*,\s*\d+)+\]/g); - 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 = ''; - }); - } - switch (promptType) { case 'DOCUMENT_SUMMARY': case 'SELECTED_TEXT_SUMMARY': case 'DOCUMENT_QUESTION': - matches = text.match(/\[\d+\][.]/g); + // Add page breaks to page citation ends with period + text = text.replace(/(\d+\])\./g, '$1.

'); break; case 'DOCUMENT_KEYWORDS': - matches = text.match(/\[\d+\]/g); + // Format bullet points with line breaks + let lines = text.split(/•\s*/).filter(Boolean); + text = lines.map(line => `• ${line.trim()}`).join('
'); 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)]; @@ -150,21 +166,36 @@ class ChatbotClient { if (pageNumber > 0 && pageNumber <= window.WebViewer.getInstance().Core.documentViewer.getDocument().getPageCount()) { const pageLink = `[${pageNumber}]`; - if (promptType === 'DOCUMENT_KEYWORDS') - text = text.replaceAll(match, `${pageLink}`); - else - text = text.replaceAll(match, `${pageLink}.
`); + text = text.replaceAll(match, `${pageLink}`); } }); } - if (promptType === 'DOCUMENT_KEYWORDS') { - let lines = text.split(/•\s*/).filter(Boolean); - text = lines.map(line => `• ${line.trim()}`).join('
'); + 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 diff --git a/webviewer-ask-ai/client/config/ui/functionMap.js b/webviewer-ask-ai/client/config/ui/functionMap.js index d762a961..8fc0f3ad 100644 --- a/webviewer-ask-ai/client/config/ui/functionMap.js +++ b/webviewer-ask-ai/client/config/ui/functionMap.js @@ -21,9 +21,15 @@ const spinOptions = { }; let askWebSDKMainDiv = null; let askWebSDKChattingDiv = null; -const systemMessages = [ +const keywords = { + summarization: ['summarize', 'summary', 'summarization'], + area: ['text', 'paragraph', 'area'], + selection: ['selected', 'selection', 'highlighted'] +}; +const spinner = new Spinner(spinOptions); +const assistantMessages = [ { - type: 'welcoming', + type: 'info', content: `Hello, I'm ${APP_SITE_NAME}. How can I help you?`, }, { @@ -34,19 +40,16 @@ const systemMessages = [ type: 'info', content: [ { - id: 0, type: 'info', content: 'Choose:', promptType: '', }, { - id: 1, type: 'question', content: 'Summarize Document', promptType: 'DOCUMENT_SUMMARY' }, { - id: 2, type: 'question', content: 'List Keywords', promptType: 'DOCUMENT_KEYWORDS' @@ -75,42 +78,33 @@ const functionMap = { askWebSDKHeaderDiv.appendChild(askWebSDKHeaderTitle); askWebSDKMainDiv.appendChild(askWebSDKHeaderDiv); - // Chatting container div with system and user messages + // Chatting container div with assistant and human messages askWebSDKChattingDiv = document.createElement('div'); askWebSDKChattingDiv.id = 'askWebSDKChattingDiv'; askWebSDKChattingDiv.className = 'askWebSDKChattingDivClass'; - systemMessages.forEach((message) => { + assistantMessages.forEach((message) => { let messageDiv = document.createElement('div'); - messageDiv.className = 'askWebSDKSystemMessageClass'; + messageDiv.className = 'askWebSDKAssistantMessageClass'; if (Array.isArray(message.content)) { message.content.forEach((contentItem) => { - let systemContentDiv = (contentItem.type === 'info') ? document.createElement('div') : document.createElement('li'); - systemContentDiv.className = (contentItem.type === 'info') ? 'askWebSDKInfoMessageClass' : 'askWebSDKQuestionMessageClass'; + let assistantContentDiv = (contentItem.type === 'info') ? document.createElement('div') : document.createElement('li'); + assistantContentDiv.className = (contentItem.type === 'info') ? 'askWebSDKInfoMessageClass' : 'askWebSDKQuestionMessageClass'; if (contentItem.type === 'question') { - systemContentDiv.onmouseover = () => { - systemContentDiv.style.textDecoration = 'underline'; - systemContentDiv.style.color = 'blue'; + assistantContentDiv.onmouseover = () => { + assistantContentDiv.style.textDecoration = 'underline'; + assistantContentDiv.style.color = 'blue'; }; - systemContentDiv.onmouseout = () => { - systemContentDiv.style.textDecoration = 'none'; - systemContentDiv.style.color = 'black'; + assistantContentDiv.onmouseout = () => { + assistantContentDiv.style.textDecoration = 'none'; + assistantContentDiv.style.color = 'black'; }; - systemContentDiv.onclick = () => { - createBubble(contentItem.content, 'user'); - const spinner = new Spinner(spinOptions); - spinner.spin(askWebSDKMainDiv); - - // Create a wrapper callback that stops the spinner after createBubble is called - const callbackWrapper = (...args) => { - createBubble(...args); - spinner.stop(); - }; - - window.chatbot.getAllText(contentItem.promptType, callbackWrapper); + assistantContentDiv.onclick = () => { + createBubble(contentItem.content, 'human'); + askQuestionByPrompt(contentItem.promptType); }; } - systemContentDiv.innerText = contentItem.content; - messageDiv.appendChild(systemContentDiv); + assistantContentDiv.innerText = contentItem.content; + messageDiv.appendChild(assistantContentDiv); }); } else messageDiv.innerText = message.content; @@ -118,6 +112,16 @@ const functionMap = { askWebSDKChattingDiv.appendChild(messageDiv); }); + // maintain the conversation sequence + if (window.conversationLog.length > 0) { + window.conversationLog.forEach((chatMessage) => { + let messageDiv = document.createElement('div'); + messageDiv.className = (chatMessage.role === 'assistant') ? 'askWebSDKAssistantMessageClass' : 'askWebSDKHumanMessageClass'; + messageDiv.innerHTML = chatMessage.content; + askWebSDKChattingDiv.appendChild(messageDiv); + }); + } + askWebSDKMainDiv.appendChild(askWebSDKChattingDiv); // Question input container div with input box and send button @@ -140,11 +144,37 @@ const functionMap = { askWebSDKQuestionButton.className = 'askWebSDKQuestionButtonClass'; askWebSDKQuestionButton.innerText = 'Send'; askWebSDKQuestionButton.onclick = () => { - if (askWebSDKQuestionInput.value.trim() !== '') { - createBubble(askWebSDKQuestionInput.value.trim(), 'user'); + let question = askWebSDKQuestionInput.value.trim(); + if (question === '') { + createBubble('Please ask a question first.', 'assistant'); + return; + } - // Start loading spinner on main div to block all interactions - const spinner = new Spinner(spinOptions); + createBubble(question, 'human'); + + // Check if the question is a summarization request + if (containsAny(question.toLowerCase(), keywords.summarization)) { + // summarize entire document + if (question.toLowerCase().includes('document') && + !containsAny(question.toLowerCase(), keywords.area)) + askQuestionByPrompt('DOCUMENT_SUMMARY'); + + // summarize selected text in document + if (containsAny(question.toLowerCase(), keywords.selection)) { + if (window.selectedText && window.selectedText.trim() !== '') + summarizeSelectedText(); + else + createBubble('Please select text in the document first.', 'assistant'); + } + + if (!question.toLowerCase().includes('document') + && !containsAny(question.toLowerCase(), keywords.selection)) { + createBubble('Please specify if you want to summarize the entire document or selected text.', 'assistant'); + } + } + // Any other questions about the document + else { + // Start spinning on main div spinner.spin(askWebSDKMainDiv); // Send question as document query @@ -152,10 +182,10 @@ const functionMap = { spinner.stop(); let responseText = window.chatbot.responseText(response); responseText = window.chatbot.formatText('DOCUMENT_QUESTION', responseText); - createBubble(responseText, 'system'); + createBubble(responseText, 'assistant'); }).catch(error => { spinner.stop(); - createBubble(`Error: ${error.message}`, 'system'); + createBubble(`Error: ${error.message}`, 'assistant'); }); } }; @@ -168,32 +198,55 @@ const functionMap = { }, // Handle selected text summary popup click 'askWebSDKPopupClick': () => { - createBubble('Summarize the selected text.', 'user'); - - // Start loading spinner for selected text summary on main div - const spinner = new Spinner(spinOptions); - spinner.spin(askWebSDKMainDiv); - - //Combine into single container for all bubble responses - window.chatbot.sendMessage('SELECTED_TEXT_SUMMARY', window.selectedText).then(response => { - spinner.stop(); - let responseText = window.chatbot.responseText(response); - responseText = window.chatbot.formatText('SELECTED_TEXT_SUMMARY', responseText); - createBubble(responseText, 'system'); - }).catch(error => { - spinner.stop(); - createBubble(`Error: ${error.message}`, 'system'); - }); + createBubble('Summarize the selected text.', 'human'); + summarizeSelectedText(); }, }; +// Function to summarize selected text +function summarizeSelectedText() { + // Start spinning on main div + spinner.spin(askWebSDKMainDiv); + + // Combine into single container for all bubble responses + window.chatbot.sendMessage('SELECTED_TEXT_SUMMARY', window.selectedText).then(response => { + spinner.stop(); + let responseText = window.chatbot.responseText(response); + responseText = window.chatbot.formatText('SELECTED_TEXT_SUMMARY', responseText); + createBubble(responseText, 'assistant'); + }).catch(error => { + spinner.stop(); + createBubble(`Error: ${error.message}`, 'assistant'); + }); +} + +// Function to ask question by prompt +function askQuestionByPrompt(prompt) { + // Start spinning on main div + spinner.spin(askWebSDKMainDiv); + + // Create a wrapper callback that stops the spinner after createBubble is called + const callbackWrapper = (...args) => { + createBubble(...args); + spinner.stop(); + }; + + window.chatbot.getAllText(prompt, callbackWrapper); +} + // Function to create a chat bubble function createBubble(content, role) { + window.conversationLog.push({ role: role, content: content }); + let messageDiv = document.createElement('div'); - messageDiv.className = (role === 'system') ? 'askWebSDKSystemMessageClass' : 'askWebSDKUserMessageClass'; + messageDiv.className = (role === 'assistant') ? 'askWebSDKAssistantMessageClass' : 'askWebSDKHumanMessageClass'; messageDiv.innerHTML = content; askWebSDKChattingDiv.appendChild(messageDiv); askWebSDKChattingDiv.scrollTop = askWebSDKChattingDiv.scrollHeight; } +function containsAny(text, list) { + return list.some(keyword => text.toLowerCase().includes(keyword.toLowerCase())); +} + export default functionMap; \ No newline at end of file diff --git a/webviewer-ask-ai/client/config/ui/styles.css b/webviewer-ask-ai/client/config/ui/styles.css index 72721af3..ffd20f7b 100644 --- a/webviewer-ask-ai/client/config/ui/styles.css +++ b/webviewer-ask-ai/client/config/ui/styles.css @@ -49,7 +49,7 @@ cursor: pointer; } -.askWebSDKSystemMessageClass { +.askWebSDKAssistantMessageClass { background-color: #ecf0f1; color: black; align-self: flex-start; @@ -59,7 +59,7 @@ max-width: 70%; } -.askWebSDKUserMessageClass { +.askWebSDKHumanMessageClass { background-color: #3498db; color: white; align-self: flex-end; diff --git a/webviewer-ask-ai/client/globals.js b/webviewer-ask-ai/client/globals.js index 78365859..0632df80 100644 --- a/webviewer-ask-ai/client/globals.js +++ b/webviewer-ask-ai/client/globals.js @@ -4,4 +4,7 @@ const ASK_WEB_SDK_ICO = " { window.selectedText = selectedText; -} \ No newline at end of file +} + +let conversationLog = []; +window.conversationLog = conversationLog; \ No newline at end of file diff --git a/webviewer-ask-ai/client/index.html b/webviewer-ask-ai/client/index.html index 828322cc..975235a2 100644 --- a/webviewer-ask-ai/client/index.html +++ b/webviewer-ask-ai/client/index.html @@ -2,7 +2,7 @@ - + diff --git a/webviewer-ask-ai/client/index.js b/webviewer-ask-ai/client/index.js index e462b17a..56fd810a 100644 --- a/webviewer-ask-ai/client/index.js +++ b/webviewer-ask-ai/client/index.js @@ -19,21 +19,20 @@ WebViewer({ // Listen for text selection events // The user can select text in the document, to be added as context for the chatbot to be processed instance.Core.documentViewer.addEventListener('textSelected', (quads, selectedText) => { - if (selectedText) { - // Set selected text as context for chatbot setSelectedText(selectedText); - } }); // Listen for document loaded event to initialize the chatbot panel instance.Core.documentViewer.addEventListener('documentLoaded', () => { - instance.UI.closeElements(['askWebSDKPanel']); - instance.UI.openElements(['askWebSDKPanel']); - instance.UI.setPanelWidth('askWebSDKPanel', 600); - // Initialize chatbot const chatbot = createChatbot(); window.chatbot = chatbot; + // Clear the conversation on new document load + window.conversationLog = []; + + instance.UI.closeElements(['askWebSDKPanel']); + instance.UI.openElements(['askWebSDKPanel']); + instance.UI.setPanelWidth('askWebSDKPanel', 600); }); }); diff --git a/webviewer-ask-ai/mainsamplesource.json b/webviewer-ask-ai/mainsamplesource.json index be4ea06e..2edc8515 100644 --- a/webviewer-ask-ai/mainsamplesource.json +++ b/webviewer-ask-ai/mainsamplesource.json @@ -1,5 +1,5 @@ { - "githubId": "", + "githubId": "d10ebe64-2a72-4792-bf63-690811a2aa75", "files": [ { "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/client/index.js", diff --git a/webviewer-ask-ai/package-lock.json b/webviewer-ask-ai/package-lock.json index fac46bc9..04d81bf3 100644 --- a/webviewer-ask-ai/package-lock.json +++ b/webviewer-ask-ai/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@langchain/core": "^1.0.6", "@langchain/openai": "^1.1.2", - "@pdftron/webviewer": "^11.9.0", + "@pdftron/webviewer": "^11.8.0", "dotenv": "^17.2.3", "spin.js": "^4.1.2" }, diff --git a/webviewer-ask-ai/package.json b/webviewer-ask-ai/package.json index b0bee753..38b4782a 100644 --- a/webviewer-ask-ai/package.json +++ b/webviewer-ask-ai/package.json @@ -18,7 +18,7 @@ "dependencies": { "@langchain/core": "^1.0.6", "@langchain/openai": "^1.1.2", - "@pdftron/webviewer": "^11.9.0", + "@pdftron/webviewer": "^11.8.0", "dotenv": "^17.2.3", "spin.js": "^4.1.2" } diff --git a/webviewer-ask-ai/server/handler.js b/webviewer-ask-ai/server/handler.js index 59f106e9..e9bd9f00 100644 --- a/webviewer-ask-ai/server/handler.js +++ b/webviewer-ask-ai/server/handler.js @@ -1,5 +1,5 @@ import { ChatOpenAI } from '@langchain/openai'; -import { HumanMessage, SystemMessage } from '@langchain/core/messages'; +import { HumanMessage, SystemMessage as AssistantMessage } from '@langchain/core/messages'; import { StringOutputParser } from '@langchain/core/output_parsers'; import dotenv from 'dotenv'; @@ -8,27 +8,27 @@ dotenv.config(); // Guard rails configuration for different prompt types const PROMPT_GUARD_RAILS = { 'DOCUMENT_SUMMARY': { - systemPrompt: 'You are a document summarizer specializing in PDF documents. Summarize the provided text concisely under 300 words. CRITICALLY IMPORTANT: For each sentence in your summary, you MUST add square brackets with the page number where that information came from (e.g., [1] for page 1, [2] for page 2, etc.). The document text is divided by page break markers in the format "<> Page N" where N is the page number. When you see "<> Page 3", all content following that marker until the next page break is from page 3. Always cite the correct page number for each fact or statement. Example: "The company reported strong earnings [1]. The new policy takes effect in January [2]."', + assistantPrompt: 'You are a document summarizer specializing in PDF documents. Summarize the provided text concisely under 300 words. CRITICALLY IMPORTANT: For each sentence in your summary, you MUST add square brackets with the page number where that information came from (e.g., [1] for page 1, [2] for page 2, etc.). The document text is divided by page break markers in the format "<> Page N" where N is the page number. When you see "<> Page 3", all content following that marker until the next page break is from page 3. Always cite the correct page number for each fact or statement. Example: "The company reported strong earnings [1]. The new policy takes effect in January [2]."', maxTokens: 500, temperature: 0.3 }, 'DOCUMENT_KEYWORDS': { - systemPrompt: 'You are a keyword extraction specialist. Create a bulleted list of the 10 most important keywords from the provided document text. CRITICALLY IMPORTANT: For each keyword, you MUST include the page number where it appears in square brackets. The document text is divided by page break markers in the format "<> Page N" where N is the page number. When you see "<> Page 3", all content following that marker until the next page break is from page 3. Format each keyword as: "• Keyword [page#]" Example: "• Federal Acquisition Regulation [1]" or "• Section 508 compliance [2]". Always cite the correct page number for each keyword.', + assistantPrompt: 'You are a keyword extraction specialist. Create a bulleted list of the 10 most important keywords from the provided document text. CRITICALLY IMPORTANT: For each keyword, you MUST include the page number where it appears in square brackets. The document text is divided by page break markers in the format "<> Page N" where N is the page number. When you see "<> Page 3", all content following that marker until the next page break is from page 3. Format each keyword as: "• Keyword [page#]" Example: "• Federal Acquisition Regulation [1]" or "• Section 508 compliance [2]". Always cite the correct page number for each keyword."', maxTokens: 500, temperature: 0.1 }, 'SELECTED_TEXT_SUMMARY': { - systemPrompt: 'You are a text summarizer for selected content. Provide a concise summary of the selected text, highlighting the main points and key information. Be clear and focused.', + assistantPrompt: 'You are a text summarizer for selected content. Provide a concise summary of the selected text, highlighting the main points and key information. Be clear and focused.', maxTokens: 500, temperature: 0.3 }, 'DOCUMENT_QUESTION': { - systemPrompt: 'You are a document Q&A assistant. Answer questions about the provided document content accurately and concisely. Use specific information from the document to support your answers. If you cannot find relevant information in the document, say so clearly. CRITICALLY IMPORTANT: For each statement or fact in your answer, you MUST add square brackets with the page number where that information came from (e.g., [1] for page 1, [2] for page 2, etc.). The document text is divided by page break markers in the format "<> Page N" where N is the page number. When you see "<> Page 3", all content following that marker until the next page break is from page 3. Always cite the correct page number for each fact or statement. Example: "The policy requires annual reviews [3]. Training must be completed within 30 days [5]."', + assistantPrompt: 'You are a document Q&A assistant. Answer questions about the provided document content accurately and concisely. Use specific information from the document to support your answers. If you cannot find relevant information in the document, say so clearly. CRITICALLY IMPORTANT: For each statement or fact in your answer, you MUST add square brackets with the page number where that information came from (e.g., [1] for page 1, [2] for page 2, etc.). The document text is divided by page break markers in the format "<> Page N" where N is the page number. When you see "<> Page 3", all content following that marker until the next page break is from page 3. Always cite the correct page number for each fact or statement. Example: "The policy requires annual reviews [3]. Training must be completed within 30 days [5]."', maxTokens: 500, temperature: 0.4 }, 'default': { - systemPrompt: 'You are a helpful assistant that helps users with PDF documents and general questions. Be concise and helpful.', + assistantPrompt: 'You are a helpful assistant that helps users with PDF documents and general questions. Be concise and helpful.', maxTokens: 1000, temperature: 0.7 } @@ -40,7 +40,7 @@ function estimateTokens(text) { } // Function to chunk text to fit within token limits -function chunkText(text, maxTokens = 12000) { // Leave room for system prompt and response +function chunkText(text, maxTokens = 12000) { // Leave room for assistant prompt and response const words = text.split(' '); const chunks = []; let currentChunk = ''; @@ -62,7 +62,7 @@ function chunkText(text, maxTokens = 12000) { // Leave room for system prompt an return chunks; } -// Function to get system prompt and settings based on prompt type +// Function to get assistant prompt and settings based on prompt type function getPromptSettings(promptType) { return PROMPT_GUARD_RAILS[promptType] || PROMPT_GUARD_RAILS['default']; } @@ -109,7 +109,7 @@ export default (app) => { }); } - const { message, promptType } = request.body; + const { message, promptType, history = [] } = request.body; if (!message || typeof message !== 'string') { return response.status(400).json({ error: 'Message is required' }); @@ -119,21 +119,21 @@ export default (app) => { const promptSettings = getPromptSettings(promptType); // Check if message is too long and handle accordingly - const estimatedTokens = estimateTokens(message) + estimateTokens(promptSettings.systemPrompt) + 500; // Buffer for history and response + const estimatedTokens = estimateTokens(message) + estimateTokens(promptSettings.assistantPrompt) + 500; // Buffer for history and response let finalContent; if (estimatedTokens > 16000) { // Leave buffer for llm limit // For keyword extraction, we can chunk the document and extract keywords from each chunk - if (promptType?.includes('keywords') || promptType?.includes('Keywords')) { + if (promptType.toLowerCase()?.includes('keywords')) { const chunks = chunkText(message, 12000); const allKeywords = []; for (let i = 0; i < chunks.length; i++) { const chunkMessages = [ - new SystemMessage(`${promptSettings.systemPrompt} This is chunk ${i + 1} of ${chunks.length}. Extract keywords from this section only.`), + new AssistantMessage(`${promptSettings.assistantPrompt} This is chunk ${i + 1} of ${chunks.length}. Extract keywords from this section only.`), new HumanMessage(chunks[i]) ]; @@ -148,7 +148,7 @@ export default (app) => { // Now consolidate all keywords into final list const consolidationMessages = [ - new SystemMessage('You are a keyword consolidation specialist. From the following keyword lists extracted from different sections of a document, create a final bulleted list of the 10 most important and representative keywords. Remove duplicates and prioritize the most significant terms.'), + new AssistantMessage('You are a keyword consolidation specialist. From the following keyword lists extracted from different sections of a document, create a final bulleted list of the 10 most important and representative keywords. Remove duplicates and prioritize the most significant terms.'), new HumanMessage(`Consolidate these keyword lists into the top 10 keywords:\n\n${allKeywords.join('\n\n---\n\n')}`) ]; @@ -163,7 +163,12 @@ export default (app) => { // For other prompt types, use first chunk with warning const chunks = chunkText(message, 12000); const messages = [ - new SystemMessage(`${promptSettings.systemPrompt}\n\nNOTE: This document was too long, so only the first section is being processed.`), + new AssistantMessage(`${promptSettings.assistantPrompt}\n\nNOTE: This document was too long, so only the first section is being processed.`), + ...history.map(msg => + msg.role === 'human' + ? new HumanMessage(msg.content) + : new AssistantMessage(`Previous assistant response: ${msg.content}`) + ), new HumanMessage(chunks[0]) ]; @@ -178,7 +183,12 @@ export default (app) => { } else { // Normal processing for documents within token limits const messages = [ - new SystemMessage(promptSettings.systemPrompt), + new AssistantMessage(promptSettings.assistantPrompt), + ...history.map(msg => + msg.role === 'human' + ? new HumanMessage(msg.content) + : new AssistantMessage(`Previous assistant response: ${msg.content}`) + ), new HumanMessage(message) ]; From faa3d3a53faf78c2c25154bd8b3ce3435991c4e5 Mon Sep 17 00:00:00 2001 From: Mohammed AbdulRahman Date: Tue, 9 Dec 2025 20:30:45 +0300 Subject: [PATCH 3/3] Updated the text selection of webviewer-ask-ai The update allows the user to select text either from a single page or multiple pages. The affected file is webviewer-ask-ai\client\index.js --- webviewer-ask-ai/client/index.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/webviewer-ask-ai/client/index.js b/webviewer-ask-ai/client/index.js index 56fd810a..6e9b9d41 100644 --- a/webviewer-ask-ai/client/index.js +++ b/webviewer-ask-ai/client/index.js @@ -16,10 +16,19 @@ WebViewer({ // Import modular components configuration from JSON file importModularComponents(instance); - // Listen for text selection events + // Set up text selection listener + const tool = instance.Core.documentViewer.getTool(instance.Core.Tools.ToolNames.TEXT_SELECT); + + // Listen for text selectionComplete event // The user can select text in the document, to be added as context for the chatbot to be processed - instance.Core.documentViewer.addEventListener('textSelected', (quads, selectedText) => { - setSelectedText(selectedText); + // The text selection can span multiple pages + tool.addEventListener('selectionComplete', (startQuad, allQuads) => { + let selectedText = ''; + Object.keys(allQuads).forEach(pageNum => { + const text = instance.Core.documentViewer.getSelectedText(pageNum); + selectedText += text + `\n<> Page ${pageNum}\n`; + }); + setSelectedText(selectedText); }); // Listen for document loaded event to initialize the chatbot panel