diff --git a/webviewer-ask-ai/README.md b/webviewer-ask-ai/README.md index 947e6ee..23ecc2b 100644 --- a/webviewer-ask-ai/README.md +++ b/webviewer-ask-ai/README.md @@ -13,7 +13,7 @@ A license key is required to run WebViewer. You can obtain a trial key in our [g ## Initial setup -Before you begin, make sure the development environment includes [Node.js](https://nodejs.org/en/). +Before you begin, make sure the development environment includes [Node.js](https://nodejs.org/en/) 20 or newer. ## Install diff --git a/webviewer-ask-ai/__mocks__/webviewer-ask-ai.mock.js b/webviewer-ask-ai/__mocks__/webviewer-ask-ai.mock.js new file mode 100644 index 0000000..ced5d52 --- /dev/null +++ b/webviewer-ask-ai/__mocks__/webviewer-ask-ai.mock.js @@ -0,0 +1,39 @@ +// This file provides mock implementations for the webviewer-ask-ai module, +// allowing developers to test and develop features without relying on actual +// API calls. + +export const MOCK_RESPONSE = { + DOCUMENT_QUESTION: 'In 2011, Rosneft undertook several social responsibility initiatives, including: 1. Support for education by providing RUB 141 million to higher education institutions and extending loans totaling RUB 3.6 million to 97 workers for educational courses. 2. Charity efforts focused on socio-economic projects, healthcare, education, culture, and sports, with a total spending of RUB 2.9 billion on charity. 3. Maintenance of social infrastructure, with spending of RUB 1.0 billion aimed at optimizing facilities for employees and communities [14][15].', + SELECTED_TEXT_SUMMARY: 'The Tuapse license area covers 12,000 square km in the Black Sea and has geological similarities to the West-Kuban Trough, a historic oil production region in Russia [6]. The Tuapse Block has undergone comprehensive 2D seismic work, with the most promising areas also analyzed using 3D seismic technology [6]. Current data indicates the presence of 20 promising structures with an estimated 8.9 billion barrels of recoverable oil resources [6].', + DOCUMENT_CONTEXTUAL_QUESTIONS: '• What strategic partnerships did Rosneft establish in 2011 for offshore exploration?\n• How did Rosneft\'s resource base change in 2011 compared to previous years?\n• What social responsibility initiatives did Rosneft undertake in 2011?' +}; + +// Registers a Playwright route interceptor that mocks all /api/chat POST requests, +// preventing real network calls to the AI backend during tests. +export const registerApiChatMock = async (page) => { + await page.route('/api/chat', async (route) => { + const requestBody = JSON.parse(route.request().postData() || '{}'); + const { promptType } = requestBody; + + let response; + switch (promptType) { + case 'DOCUMENT_CONTEXTUAL_QUESTIONS': + response = MOCK_RESPONSE.DOCUMENT_CONTEXTUAL_QUESTIONS; + break; + case 'DOCUMENT_QUESTION': + response = MOCK_RESPONSE.DOCUMENT_QUESTION; + break; + case 'SELECTED_TEXT_SUMMARY': + response = MOCK_RESPONSE.SELECTED_TEXT_SUMMARY; + break; + default: + response = ''; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ response }), + }); + }); +}; \ No newline at end of file diff --git a/webviewer-ask-ai/__tests__/e2e/webviewer-ask-ai.spec.js b/webviewer-ask-ai/__tests__/e2e/webviewer-ask-ai.spec.js new file mode 100644 index 0000000..8e9188d --- /dev/null +++ b/webviewer-ask-ai/__tests__/e2e/webviewer-ask-ai.spec.js @@ -0,0 +1,131 @@ +// This file contains end-to-end tests for the WebViewer Ask AI sample application, +// using the Playwright testing framework. + +import { test, expect } from '@playwright/test'; +import { MOCK_RESPONSE, registerApiChatMock } from '../../__mocks__/webviewer-ask-ai.mock.js'; + +const question = 'What social responsibility initiatives did Rosneft undertake in 2011?'; + +// Register the API chat mock before each test +// to ensure consistent and predictable responses +// from the chatbot during testing. +test.beforeEach(async ({ page }) => { + await registerApiChatMock(page); +}); + +// Validate the chatbot toggle button visibility. +test('Chatbot toggle button visibility', async ({ page }) => { + await page.goto('/client/index.html'); + + const toggle = page.locator('button[data-element="askWebSDKPanelToggle"]'); + await toggle.waitFor({ state: 'visible' }); + await expect(toggle).toBeVisible(); +}); + +// Validate the chatbot panel visibility. +test('Chatbot panel visibility', async ({ page }) => { + await page.goto('/client/index.html'); + + const panel = page.locator('div.ModularPanel[data-element="askWebSDKPanel"]'); + await panel.waitFor({ state: 'visible' }); + await expect(panel).toBeVisible(); +}); + +// Validate the summarizing selection button visibility. +test('Summarizing selection button visibility', async ({ page }) => { + await page.goto('/client/index.html'); + await simulateDocumentTextSelection(page, { + pageNumber: 2, + start: { x: 56.69320848, y: 32.40185332 }, + end: { x: 105.11567193, y: 40.06439572 }, + }); + + const popupButton = page.locator('button[data-element="askWebSDKButton"]'); + await popupButton.waitFor({ state: 'visible' }); + await expect(popupButton).toBeVisible(); +}); + +// Simulates user asking free question within the chatbot panel +test('Ask free question', async ({ page }) => { + await page.goto('/client/index.html'); + + // Locate the question input field, enter a question, and submit it by pressing 'Enter' + const questionInput = page.locator('#askWebSDKQuestionInput'); + await questionInput.fill(question); + await questionInput.press('Enter'); + + // Validate that the assistant's response contains the expected text from the mock response + const assistantResponse = page.locator('.askWebSDKAssistantMessageClass').last(); + await expect(assistantResponse).toContainText(MOCK_RESPONSE.DOCUMENT_QUESTION); +}); + +// Simulates user selecting text on the document and asking the chatbot to summarize it +test('Summarize selected text', async ({ page }) => { + await page.goto('/client/index.html'); + await simulateDocumentTextSelection(page, { + pageNumber: 6, + start: { x: 179.72459999999998, y: 536.4002 }, + end: { x: 141.165, y: 415.9352 }, + }); + + // Click the "Summarize selection" button in the TextPopup + const popupButton = page.locator('button[data-element="askWebSDKButton"]'); + await popupButton.waitFor({ state: 'visible' }); + await popupButton.click(); + + // Validate that the assistant's response contains the expected text from the mock response + const assistantResponse = page.locator('.askWebSDKAssistantMessageClass').last(); + await expect(assistantResponse).toContainText(MOCK_RESPONSE.SELECTED_TEXT_SUMMARY); +}); + +// Simulates user hiding and showing the chatbot panel +// via clicking the toggle button in the header. +// Chatbot panel should maintain the visibility of the conversation. +test('Hide/Show chatbot panel', async ({ page }) => { + await page.goto('/client/index.html'); + + // Locate the question input field, enter a question, and submit it by pressing 'Enter' + const questionInput = page.locator('#askWebSDKQuestionInput'); + await questionInput.fill(question); + await questionInput.press('Enter'); + + // Locate the toggle button and chatbot panel + const toggle = page.locator('button[data-element="askWebSDKPanelToggle"]'); + const panel = page.locator('div.ModularPanel[data-element="askWebSDKPanel"]'); + + // Hide the chatbot panel + await toggle.click(); + await expect(panel).not.toBeVisible(); + + // Show the chatbot panel again + await toggle.click(); + await expect(panel).toBeVisible(); +}); + +// Helper function to select document text and trigger the TextPopup. +const simulateDocumentTextSelection = async (page, { pageNumber, start, end }) => { + await page.locator('div.ModularPanel[data-element="askWebSDKPanel"]').waitFor({ state: 'visible' }); + + await page.evaluate(({ pageNumber, start, end }) => { + const instance = globalThis.WebViewer.getInstance(); + const core = instance.Core; + const UI = instance.UI; + const documentViewer = core.documentViewer; + const textSelectTool = documentViewer.getTool(core.Tools.ToolNames.TEXT_SELECT); + + documentViewer.setCurrentPage(pageNumber); + UI.setToolMode(core.Tools.ToolNames.TEXT_SELECT); + textSelectTool.select({ pageNumber, ...start }, { pageNumber, ...end }); + }, { pageNumber, start, end }); + + await page.waitForFunction(() => { + const instance = globalThis.WebViewer.getInstance(); + const selectedText = instance?.Core?.documentViewer?.getSelectedText?.(); + return typeof selectedText === 'string' && selectedText.trim().length > 0; + }); + + await page.evaluate(() => { + const instance = globalThis.WebViewer.getInstance(); + instance.UI.openElements(['textPopup']); + }); +}; \ No newline at end of file diff --git a/webviewer-ask-ai/client/chatbot/chatbot.js b/webviewer-ask-ai/client/chatbot/chatbot.js index d59f67b..6139e7f 100644 --- a/webviewer-ask-ai/client/chatbot/chatbot.js +++ b/webviewer-ask-ai/client/chatbot/chatbot.js @@ -29,13 +29,8 @@ class Chatbot { const message = messagesHistory[i]; let messageTokenCount; - try { - // Use simple character-based estimation for token counting - messageTokenCount = Math.ceil(message.content.length / 4); - } catch (error) { - // Fallback to estimation - messageTokenCount = Math.ceil(message.content.length / 4); - } + // Use simple character-based estimation for token counting + messageTokenCount = Math.ceil(message.content.length / 4); if (tokenCount + messageTokenCount <= maxTokens) { trimmedHistory.unshift(message); @@ -48,57 +43,53 @@ class Chatbot { } async sendMessage(promptLine, message) { - try { - // For document-level operations, use increased token limit to preserve messages history - // Adjust token limits based on prompt type to balance document content and messages history - let maxTokens = this.messagesHistoryOptions.maxTokens || 8000; - - // For document questions, we need more room for messages history since we're sending full document - if (promptLine.includes('DOCUMENT_')) - maxTokens = Math.max(maxTokens, 8000); // Ensure minimum 8000 tokens for document questions - const messagesHistoryToSend = this.messagesHistoryOptions.useEmpty ? [] : await this.trimHistoryForTokenLimit(this.messagesHistory, maxTokens); - - const response = await fetch('/api/chat', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - message: message, - promptType: promptLine, - history: messagesHistoryToSend - }) - }); - - if (!response.ok) - throw new Error(`HTTP error! status: ${response.status}`); - - const data = await response.json(); - - // Update messages history only if not explicitly disabled - if (!this.messagesHistoryOptions.skipUpdate) { - // For document queries, extract only the question part to avoid storing redundant document content - let historyMessage = message; - if (promptLine.includes('DOCUMENT_')) { - // Extract question from document queries to avoid token waste - const questionMatch = message.match(/(?:Human Question|Question): (.+?)\n\nDocument Content:/); - if (questionMatch) - historyMessage = questionMatch[1]; - else - // Fallback: use first 200 chars if pattern not found, avoiding full document - historyMessage = message.length > 200 ? message.substring(0, 200) + '... [document content excluded from history]' : message; - } + // For document-level operations, use increased token limit to preserve messages history + // Adjust token limits based on prompt type to balance document content and messages history + let maxTokens = this.messagesHistoryOptions.maxTokens || 8000; + + // For document questions, we need more room for messages history since we're sending full document + if (promptLine.includes('DOCUMENT_')) + maxTokens = Math.max(maxTokens, 8000); // Ensure minimum 8000 tokens for document questions + const messagesHistoryToSend = this.messagesHistoryOptions.useEmpty ? [] : await this.trimHistoryForTokenLimit(this.messagesHistory, maxTokens); + + const response = await fetch('/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: message, + promptType: promptLine, + history: messagesHistoryToSend + }) + }); - this.messagesHistory.push( - { role: 'human', content: `${promptLine}: ${historyMessage}` }, - { role: 'assistant', content: data.response } - ); + if (!response.ok) + throw new Error(`HTTP error! status: ${response.status}`); + + const data = await response.json(); + + // Update messages history only if not explicitly disabled + if (!this.messagesHistoryOptions.skipUpdate) { + // For document queries, extract only the question part to avoid storing redundant document content + let historyMessage = message; + if (promptLine.includes('DOCUMENT_')) { + // Extract question from document queries to avoid token waste + const questionMatch = message.match(/(?:Human Question|Question): (.+?)\n\nDocument Content:/); + if (questionMatch) + historyMessage = questionMatch[1]; + else + // Fallback: use first 200 chars if pattern not found, avoiding full document + historyMessage = message.length > 200 ? message.substring(0, 200) + '... [document content excluded from history]' : message; } - return data.response; - } catch (error) { - throw error; + this.messagesHistory.push( + { role: 'human', content: `${promptLine}: ${historyMessage}` }, + { role: 'assistant', content: data.response } + ); } + + return data.response; } // Prepare a message, considering contextual question or history question @@ -151,7 +142,7 @@ class Chatbot { askQuestionByPrompt(prompt, question = null) { // Start spinning on main div - spinner.spin(askWebSDKMainDiv); + spinner.spin(globalThis.askWebSDKMainDiv); // Create a wrapper callback that stops the spinner after bubble is called const callbackWrapper = (...args) => { @@ -169,7 +160,7 @@ class Chatbot { // DOCUMENT_QUESTION async summarizeTextByPrompt(prompt, text) { // Start spinning on main div - spinner.spin(askWebSDKMainDiv); + spinner.spin(globalThis.askWebSDKMainDiv); // Combine into single container for all bubble responses let responseText = ''; @@ -204,9 +195,11 @@ class Chatbot { let updatedCount = 0; questionsLIs.forEach((configAndLiTags) => { - if (configAndLiTags[0] && configAndLiTags[0].promptType === 'DOCUMENT_CONTEXTUAL_QUESTION_EXACTLY') { + if (configAndLiTags[0]?.promptType === 'DOCUMENT_CONTEXTUAL_QUESTION_EXACTLY') { - if (this.questionsContextuallySound[index] !== undefined) { + if (this.questionsContextuallySound[index] === undefined) { + console.warn(`Question ${index + 1} is undefined! Available questions:`, this.questionsContextuallySound); + } else { let li = configAndLiTags[1]; if (li) li.innerText = this.questionsContextuallySound[index]; @@ -218,8 +211,7 @@ class Chatbot { configItem.content = this.questionsContextuallySound[index]; updatedCount++; - } else - console.warn(`Question ${index + 1} is undefined! Available questions:`, this.questionsContextuallySound); + } index++; } @@ -248,8 +240,8 @@ class Chatbot { let messageDiv = document.createElement('div'); messageDiv.className = (role === 'assistant') ? 'askWebSDKAssistantMessageClass' : 'askWebSDKHumanMessageClass'; messageDiv.innerHTML = content; - askWebSDKChattingDiv.appendChild(messageDiv); - askWebSDKChattingDiv.scrollTop = askWebSDKChattingDiv.scrollHeight; + globalThis.askWebSDKChattingDiv.appendChild(messageDiv); + globalThis.askWebSDKChattingDiv.scrollTop = globalThis.askWebSDKChattingDiv.scrollHeight; } }; diff --git a/webviewer-ask-ai/client/globals.js b/webviewer-ask-ai/client/globals.js index 182e900..fd06b68 100644 --- a/webviewer-ask-ai/client/globals.js +++ b/webviewer-ask-ai/client/globals.js @@ -8,8 +8,11 @@ let clipboard = ''; // Chatbot panel div elements let askWebSDKMainDiv = null; +globalThis.askWebSDKMainDiv = askWebSDKMainDiv; let askWebSDKChattingDiv = null; +globalThis.askWebSDKChattingDiv = askWebSDKChattingDiv; let assistantContentDiv = null; +globalThis.assistantContentDiv = assistantContentDiv; // Chatbot panel conversation log // to keep track of assistant and human messages diff --git a/webviewer-ask-ai/client/ui/functionMap.js b/webviewer-ask-ai/client/ui/functionMap.js index 1fb67d8..463535b 100644 --- a/webviewer-ask-ai/client/ui/functionMap.js +++ b/webviewer-ask-ai/client/ui/functionMap.js @@ -1,3 +1,23 @@ +function handleSummarization(question) { + // summarize entire document + if (question.toLowerCase().includes('document') && + !containsAny(question, configData.KEYWORDS.area)) { + chatbot.askQuestionByPrompt('DOCUMENT_SUMMARY'); + } + // summarize selected text (clipboard) in document + if (containsAny(question, configData.KEYWORDS.selection)) { + if (clipboard && clipboard.trim() !== '') + chatbot.summarizeTextByPrompt('SELECTED_TEXT_SUMMARY', clipboard); + else + chatbot.bubble('Please select text in the document first.', 'assistant'); + } + + if (!question.toLowerCase().includes('document') + && !containsAny(question, configData.KEYWORDS.selection)) { + chatbot.bubble('Please specify if you want to summarize the entire document or selected text.', 'assistant'); + } +} + const functionMap = { // Render the WebViewer chat panel 'askWebSDKPanelRender': () => { @@ -13,9 +33,9 @@ const functionMap = { } // The main container div - askWebSDKMainDiv = document.createElement('div'); - askWebSDKMainDiv.id = 'askWebSDKMainDiv'; - askWebSDKMainDiv.className = 'askWebSDKMainDivClass'; + globalThis.askWebSDKMainDiv = document.createElement('div'); + globalThis.askWebSDKMainDiv.id = 'askWebSDKMainDiv'; + globalThis.askWebSDKMainDiv.className = 'askWebSDKMainDivClass'; //Top and Bottom container divs const askWebSDKQuestionDivTop = document.createElement('div'); @@ -32,9 +52,9 @@ const functionMap = { askWebSDKQuestionDivTop.appendChild(askWebSDKHeaderDiv); // Chatting container div with assistant and human messages - askWebSDKChattingDiv = document.createElement('div'); - askWebSDKChattingDiv.id = 'askWebSDKChattingDiv'; - askWebSDKChattingDiv.className = 'askWebSDKChattingDivClass'; + globalThis.askWebSDKChattingDiv = document.createElement('div'); + globalThis.askWebSDKChattingDiv.id = 'askWebSDKChattingDiv'; + globalThis.askWebSDKChattingDiv.className = 'askWebSDKChattingDivClass'; // Initial assistant messages configData.ASSISTANT_MESSAGES.forEach((message) => { @@ -43,37 +63,36 @@ const functionMap = { if (Array.isArray(message.content)) { message.content.forEach((contentItem) => { // Create different elements for info and question types - assistantContentDiv = (contentItem.type === 'info') ? document.createElement('div') : document.createElement('li'); - assistantContentDiv.className = (contentItem.type === 'info') ? 'askWebSDKInfoMessageClass' : 'askWebSDKQuestionMessageClass'; + globalThis.assistantContentDiv = (contentItem.type === 'info') ? document.createElement('div') : document.createElement('li'); + globalThis.assistantContentDiv.className = (contentItem.type === 'info') ? 'askWebSDKInfoMessageClass' : 'askWebSDKQuestionMessageClass'; if (contentItem.type === 'question') { // Store question LIs for later updating with contextual questions let configAndLiTags = []; - configAndLiTags.push(contentItem); //Stores type, content, promptType - configAndLiTags.push(assistantContentDiv); //Stores the actual LI element + configAndLiTags.push(contentItem, globalThis.assistantContentDiv); // Stores type, content, promptType and the actual LI element questionsLIs.push(configAndLiTags); - assistantContentDiv.onmouseover = () => { - assistantContentDiv.className = 'askWebSDKQuestionMessageHoverClassOnMouseOver'; + globalThis.assistantContentDiv.onmouseover = () => { + globalThis.assistantContentDiv.className = 'askWebSDKQuestionMessageHoverClassOnMouseOver'; }; - assistantContentDiv.onmouseout = () => { - assistantContentDiv.className = 'askWebSDKQuestionMessageHoverClassOnMouseOut'; + globalThis.assistantContentDiv.onmouseout = () => { + globalThis.assistantContentDiv.className = 'askWebSDKQuestionMessageHoverClassOnMouseOut'; }; - assistantContentDiv.onclick = () => { + globalThis.assistantContentDiv.onclick = () => { chatbot.bubble(contentItem.content, 'human'); // Pass question content for all question types, including contextual questions chatbot.askQuestionByPrompt(contentItem.promptType, contentItem.content); - assistantContentDiv.className = 'askWebSDKQuestionMessageHoverClassOnClick'; + globalThis.assistantContentDiv.className = 'askWebSDKQuestionMessageHoverClassOnClick'; }; } - assistantContentDiv.innerText = contentItem.content; - messageDiv.appendChild(assistantContentDiv); + globalThis.assistantContentDiv.innerText = contentItem.content; + messageDiv.appendChild(globalThis.assistantContentDiv); }); } else messageDiv.innerText = `${message.content}`; - askWebSDKChattingDiv.appendChild(messageDiv); + globalThis.askWebSDKChattingDiv.appendChild(messageDiv); }); // maintain the chatbot panel conversation sequence @@ -82,11 +101,11 @@ const functionMap = { let messageDiv = document.createElement('div'); messageDiv.className = (chatMessage.role === 'assistant') ? 'askWebSDKAssistantMessageClass' : 'askWebSDKHumanMessageClass'; messageDiv.innerHTML = chatMessage.content; - askWebSDKChattingDiv.appendChild(messageDiv); + globalThis.askWebSDKChattingDiv.appendChild(messageDiv); }); } - askWebSDKQuestionDivTop.appendChild(askWebSDKChattingDiv); + askWebSDKQuestionDivTop.appendChild(globalThis.askWebSDKChattingDiv); // Question input container div with input box and send button let askWebSDKQuestionDiv = document.createElement('div'); askWebSDKQuestionDiv.id = 'askWebSDKQuestionDiv'; @@ -106,6 +125,7 @@ const functionMap = { askWebSDKQuestionButton.id = 'askWebSDKQuestionButton'; askWebSDKQuestionButton.className = 'askWebSDKQuestionButtonClass'; askWebSDKQuestionButton.innerText = 'Send'; + askWebSDKQuestionButton.onclick = () => { let question = askWebSDKQuestionInput.value.trim(); if (question === '') { @@ -117,32 +137,14 @@ const functionMap = { // Check if the question is a summarization request if (containsAny(question, configData.KEYWORDS.summarization)) { - // summarize entire document - if (question.toLowerCase().includes('document') && - !containsAny(question, configData.KEYWORDS.area)) { - chatbot.askQuestionByPrompt('DOCUMENT_SUMMARY'); - } - // summarize selected text (clipboard) in document - if (containsAny(question, configData.KEYWORDS.selection)) { - if (clipboard && clipboard.trim() !== '') - chatbot.summarizeTextByPrompt('SELECTED_TEXT_SUMMARY', clipboard); - else - chatbot.bubble('Please select text in the document first.', 'assistant'); - } - - if (!question.toLowerCase().includes('document') - && !containsAny(question, configData.KEYWORDS.selection)) { - chatbot.bubble('Please specify if you want to summarize the entire document or selected text.', 'assistant'); - } + handleSummarization(question); } // Any other questions about the document - else { - // Check if this is a history-related question - if (containsAny(question, configData.KEYWORDS.history)) - // Use document-aware flow for history questions - chatbot.askQuestionByPrompt('DOCUMENT_HISTORY_QUESTION', question); - else - chatbot.summarizeTextByPrompt('DOCUMENT_QUESTION', question); + else if (containsAny(question, configData.KEYWORDS.history)) { + // Use document-aware flow for history questions + chatbot.askQuestionByPrompt('DOCUMENT_HISTORY_QUESTION', question); + } else { + chatbot.summarizeTextByPrompt('DOCUMENT_QUESTION', question); } askWebSDKQuestionInput.value = ''; // Clear input box @@ -152,10 +154,10 @@ const functionMap = { askWebSDKQuestionDiv.appendChild(askWebSDKQuestionButton); askWebSDKQuestionDivBottom.appendChild(askWebSDKQuestionDiv); - askWebSDKMainDiv.appendChild(askWebSDKQuestionDivTop); - askWebSDKMainDiv.appendChild(askWebSDKQuestionDivBottom); + globalThis.askWebSDKMainDiv.appendChild(askWebSDKQuestionDivTop); + globalThis.askWebSDKMainDiv.appendChild(askWebSDKQuestionDivBottom); - return askWebSDKMainDiv; + return globalThis.askWebSDKMainDiv; }, // Handle selected text (clipboard) summary popup click 'askWebSDKPopupClick': () => { diff --git a/webviewer-ask-ai/mainsamplesource.json b/webviewer-ask-ai/mainsamplesource.json index d14031b..9dafecd 100644 --- a/webviewer-ask-ai/mainsamplesource.json +++ b/webviewer-ask-ai/mainsamplesource.json @@ -47,7 +47,7 @@ "reason": "" }, { - "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/server/server.js", + "path": "ApryseSDK/webviewer-samples/refs/heads/main/webviewer-ask-ai/server/serve.js", "description": "", "reason": "" }, diff --git a/webviewer-ask-ai/package-lock.json b/webviewer-ask-ai/package-lock.json index 2d2b9bc..9732525 100644 --- a/webviewer-ask-ai/package-lock.json +++ b/webviewer-ask-ai/package-lock.json @@ -9,16 +9,20 @@ "version": "1.0.0", "hasInstallScript": true, "dependencies": { - "@langchain/core": "^1.1.17", - "@langchain/openai": "^1.2.3", - "@pdftron/webviewer": "^11.10.0", - "dotenv": "^17.2.3", + "@langchain/core": "^1.1.36", + "@langchain/openai": "^1.3.1", + "@pdftron/webviewer": "^11.11.0", + "dotenv": "^17.3.1", "express": "^5.2.1" }, "devDependencies": { + "@playwright/test": "^1.58.2", "body-parser": "^2.2.2", - "fs-extra": "^11.3.3", + "fs-extra": "^11.3.4", "open": "^11.0.0" + }, + "engines": { + "node": ">=20" } }, "node_modules/@cfworker/json-schema": { @@ -28,20 +32,21 @@ "license": "MIT" }, "node_modules/@langchain/core": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.17.tgz", - "integrity": "sha512-g7/kcKbKEwNZSyyT7aT0utxn7wTOtKErqz0cL6VjrV4v/aOb9g+dKcfj17YkSm42YQmJp/rB2IXGc17vQPEBqA==", + "version": "1.1.36", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.36.tgz", + "integrity": "sha512-9NWsdzU3uZD13lJwunXK0t6SIwew+UwcbHggW5yUdaiMmzKeNkDpp1lRD6p49N8+D0Vv4qmQBEKB4Ukh2jfnvw==", "license": "MIT", "dependencies": { "@cfworker/json-schema": "^4.0.2", + "@standard-schema/spec": "^1.1.0", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": ">=0.4.0 <1.0.0", + "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", - "uuid": "^10.0.0", + "uuid": "^11.1.0", "zod": "^3.25.76 || ^4" }, "engines": { @@ -49,26 +54,48 @@ } }, "node_modules/@langchain/openai": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.2.3.tgz", - "integrity": "sha512-+bKR4+Obz5a/NHEw0bAm3f/s4k0cXc/g46ZRRXqjcyDYP+9wFarItvGNn6DEEk5S7pGp1QqApAQNt9IZk1Ic1Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.3.1.tgz", + "integrity": "sha512-6yN3XFRUKUsGREGk4VtCvnMp5NHh2gWujiuWdn/G7cCeHboYrdKLWnwGqopuFOm7Tivv423gtMN1GQ7EJ3kg+g==", "license": "MIT", "dependencies": { "js-tiktoken": "^1.0.12", - "openai": "^6.16.0", + "openai": "^6.27.0", "zod": "^3.25.76 || ^4" }, "engines": { "node": ">=20" }, "peerDependencies": { - "@langchain/core": "^1.0.0" + "@langchain/core": "^1.1.36" } }, "node_modules/@pdftron/webviewer": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-11.10.0.tgz", - "integrity": "sha512-v6uzVCOd18/FCHHOt/0GAzMCSnIX2LADFiefR0jJ/oh0kGPyaN8pZqFrZo+LM8kR8xwAtTRu+pr9eV11LkCKpA==" + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-11.11.0.tgz", + "integrity": "sha512-KHF7ldPGOV7wpyRaAWennKVJsv6ZbGBrzD4uDsXu7UR8XGbZRCE5QpD/0S2xyKEMoLmmmUrASy6RHYts91fAsQ==" + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" }, "node_modules/@types/uuid": { "version": "10.0.0", @@ -212,54 +239,17 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "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", @@ -336,9 +326,9 @@ } }, "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==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "dependencies": { @@ -388,9 +378,9 @@ } }, "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==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -562,9 +552,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -576,6 +566,21 @@ "node": ">=14.14" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -641,15 +646,6 @@ "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", @@ -780,9 +776,9 @@ "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==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -818,13 +814,13 @@ } }, "node_modules/langsmith": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.4.10.tgz", - "integrity": "sha512-l9QP/a7RXBXdaoAnNx99X+TK8aul8Qe4us1oCybdMgDmYMLT5PAwlJactvSdTlT8NOeSoFThYa2N7ijznBNe9w==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.5.15.tgz", + "integrity": "sha512-S20JnYmIgqGBjA/WEn12ZZJjqd03O5wd8K9KgGBvsKXQBn0bYuFrr1w20L37PpcMmX3/cftpgJ6g2y8KoEmHLw==", "license": "MIT", "dependencies": { "@types/uuid": "^10.0.0", - "chalk": "^4.1.2", + "chalk": "^5.6.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "semver": "^7.6.3", @@ -834,7 +830,8 @@ "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", - "openai": "*" + "openai": "*", + "ws": ">=7" }, "peerDependenciesMeta": { "@opentelemetry/api": { @@ -848,9 +845,25 @@ }, "openai": { "optional": true + }, + "ws": { + "optional": true } } }, + "node_modules/langsmith/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -985,9 +998,9 @@ } }, "node_modules/openai": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.16.0.tgz", - "integrity": "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==", + "version": "6.33.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.33.0.tgz", + "integrity": "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw==", "license": "Apache-2.0", "bin": { "openai": "bin/cli" @@ -1052,15 +1065,47 @@ } }, "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==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/powershell-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", @@ -1088,9 +1133,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -1162,9 +1207,9 @@ "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==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1311,18 +1356,6 @@ "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", @@ -1366,16 +1399,16 @@ } }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vary": { diff --git a/webviewer-ask-ai/package.json b/webviewer-ask-ai/package.json index e078062..0b37a91 100644 --- a/webviewer-ask-ai/package.json +++ b/webviewer-ask-ai/package.json @@ -6,19 +6,26 @@ "main": "index.js", "scripts": { "postinstall": "node tools/copy-webviewer-files.js", - "start": "node server/serve.js" + "start": "node server/serve.js", + "test:e2e": "npx playwright test", + "test:e2e:ui": "npx playwright test --ui", + "test:e2e:debug": "npx playwright test --debug" + }, + "engines": { + "node": ">=20" }, "author": "Apryse Systems Inc.", "devDependencies": { + "@playwright/test": "^1.58.2", "body-parser": "^2.2.2", - "fs-extra": "^11.3.3", + "fs-extra": "^11.3.4", "open": "^11.0.0" }, "dependencies": { - "@langchain/core": "^1.1.17", - "@langchain/openai": "^1.2.3", - "@pdftron/webviewer": "^11.10.0", - "dotenv": "^17.2.3", + "@langchain/core": "^1.1.36", + "@langchain/openai": "^1.3.1", + "@pdftron/webviewer": "^11.11.0", + "dotenv": "^17.3.1", "express": "^5.2.1" } } diff --git a/webviewer-ask-ai/playwright.config.js b/webviewer-ask-ai/playwright.config.js new file mode 100644 index 0000000..308b074 --- /dev/null +++ b/webviewer-ask-ai/playwright.config.js @@ -0,0 +1,22 @@ +// Playwright configuration for E2E tests +// See https://playwright.dev/docs/test-configuration + +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: '__tests__', + fullyParallel: true, + reporter: 'html', + retries: 0, + use: { + headless: true, + baseURL: 'http://localhost:4040/', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + webServer: { + command: 'npm start -- --no-open', + url: 'http://localhost:4040/client/index.html', + reuseExistingServer: true, + }, +}); \ No newline at end of file diff --git a/webviewer-ask-ai/server/handler.js b/webviewer-ask-ai/server/handler.js index 67316f2..ec4098b 100644 --- a/webviewer-ask-ai/server/handler.js +++ b/webviewer-ask-ai/server/handler.js @@ -2,9 +2,10 @@ import { HumanMessage, SystemMessage as AssistantMessage } from '@langchain/core import dotenv from 'dotenv'; import LLMManager from './llmManager.js'; import logger from './logger.js'; -import { readFileSync } from 'fs'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { randomUUID } from 'node:crypto'; dotenv.config(); @@ -19,8 +20,7 @@ const GUARD_RAILS = configData.GUARD_RAILS; // Create LLMManager instance const llmManager = new LLMManager(); -export default (app) => { - +export default function registerHandlers(app) { // Initialize LangChain on startup llmManager.initialize(); @@ -91,7 +91,7 @@ export default (app) => { // Update llm settings // Reduced maxTokens to prevent truncation and repetition - llmManager.tuneSettings({maxTokens: 120}); + llmManager.tuneSettings({ maxTokens: 120 }); // Execute keyword extraction for this chunk const chunkContent = await llmManager.executeMessages(chunkMessages); @@ -106,7 +106,7 @@ export default (app) => { ]; // Update llm settings - llmManager.tuneSettings({maxTokens: 200}); + llmManager.tuneSettings({ maxTokens: 200 }); // Execute consolidation messages to get final keywords finalContent = await llmManager.executeMessages(consolidationMessages); @@ -124,7 +124,7 @@ export default (app) => { ]; // Update llm settings - llmManager.tuneSettings({maxTokens: guardRail.LLM.Settings.maxTokens}); + llmManager.tuneSettings({ maxTokens: guardRail.LLM.Settings.maxTokens }); // Execute messages with only the first chunk finalContent = await llmManager.executeMessages(messages); @@ -145,7 +145,7 @@ export default (app) => { await logger.logContextualQuestionDebug(promptType, message, guardRail, history, llmManager.getTokenCount.bind(llmManager)); // Update llm settings - llmManager.tuneSettings({maxTokens: guardRail.LLM.Settings.maxTokens}); + llmManager.tuneSettings({ maxTokens: guardRail.LLM.Settings.maxTokens }); // Execute messages normally finalContent = await llmManager.executeMessages(messages); @@ -157,8 +157,16 @@ export default (app) => { response.status(200).json({ response: cleanResponse }); } catch (error) { + const requestId = randomUUID(); + const isDebugMode = process.env.DEBUG_ERRORS === 'true' || process.env.NODE_ENV === 'development'; + + logger.error(`Chat API error [requestId=${requestId}]`, error); + response.status(500).json({ - error: 'An error occurred while processing your request' + error: isDebugMode + ? (error?.message || 'An error occurred while processing the chat request') + : 'An internal server error occurred while processing the chat request', + requestId }); } }); diff --git a/webviewer-ask-ai/server/serve.js b/webviewer-ask-ai/server/serve.js index afd1499..267203d 100644 --- a/webviewer-ask-ai/server/serve.js +++ b/webviewer-ask-ai/server/serve.js @@ -1,5 +1,4 @@ // 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'; @@ -7,25 +6,35 @@ import handler from './handler.js'; import dotenv from 'dotenv'; dotenv.config(); - -const app = express(); +const port = Number(process.env.PORT) || 4040; +// For testing purposes only, you can prevent the server from +// opening the browser automatically when it starts by either: +// - setting the environment variable NO_OPEN=true, or +// - passing the CLI flag --no-open +const noOpenEnv = String(process.env.NO_OPEN || '').toLowerCase() === 'true'; +const noOpenFlag = process.argv.includes('--no-open'); +const shouldOpenBrowser = !(noOpenEnv || noOpenFlag); // Use JSON body parser for API endpoints +const app = express(); app.use(bodyParser.json()); app.use(bodyParser.text()); + // For statically serving 'client' folder app.use('/client', express.static('client')); -// Serve config directory for client-side access +// Server shared browser mock modules app.use('/config', express.static('config')); handler(app); // Run server -app.listen(process.env.PORT, 'localhost', (err) => { +app.listen(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`); + console.info(`Server is listening at http://localhost:${port}/client/index.html`); + if (shouldOpenBrowser) { + open(`http://localhost:${port}/client/index.html`); + } } }); \ No newline at end of file