window.onload = async function() { // Initialize the PDF viewer with configuration options. // DsPdfViewer.LicenseKey can be used to set the license key (currently commented out) const viewer = new DsPdfViewer("#viewer", { supportApi: getSupportApiSettings(), // apply SupportApi settings for editing support restoreViewStateOnLoad: false // Prevent restoring the viewer's state on load (default view state) }); // Add default side to the viewer viewer.addDefaultPanels(); // Configure toolbar buttons for different layouts (default, mobile, fullscreen) viewer.toolbarLayout.viewer = { // Default layout with common buttons default: ["open", "save", "$navigation", "$split", "$zoom", "$split", 'doc-title', "about"], // Mobile layout with a simplified toolbar for smaller screens mobile: ["open", "save", "$navigation", "$split", "$zoom", "$split", 'doc-title', "about"], // Fullscreen layout with fullscreen-specific toolbar elements fullscreen: ["$fullscreen", "open", "save", "$navigation", "$split", "$zoom", "$split", 'doc-title', "about"] }; // Apply the toolbar layout configuration to the viewer viewer.applyToolbarLayout(); // Add the AI tools panel to the viewer for enhanced functionalities addAiToolsPanel(viewer); // Define the URL of the PDF document to open var pdfUrlToOpen = "/document-solutions/javascript-pdf-viewer/demos/product-bundles/assets/pdf/wetlands.pdf"; // Open the specified PDF document in the viewer await viewer.open(pdfUrlToOpen); // Set the zoom mode to "Fit Width" (Accepted values are: 0 - Value, 1 - Page Width, 2 - Whole Page) viewer.zoomMode = 1; }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Text Summarizer using ChatGPT.</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./src/styles.css"> <script src="/document-solutions/javascript-pdf-viewer/demos/product-bundles/build/dspdfviewer.js"></script> <script src="/document-solutions/javascript-pdf-viewer/demos/product-bundles/build/wasmSupportApi.js"></script> <script src="/document-solutions/javascript-pdf-viewer/demos/resource/js/init.js"></script> <script src="./src/aitools.js"></script> <script src="./src/ui.js"></script> <script src="./src/app.js"></script> </head> <body> <div id="viewer"></div> </body> </html>
#viewer { height: 100%; } .sample-ai-tools-panel label { margin-bottom: 10px; } .sample-ai-tools-panel select, .sample-ai-tools-panel button { width: 100%; height: 30px; line-height: 30px; text-align: center; } .sample-ai-tools-panel .gc-toggle input { width: 20px; height: 20px; margin-left: 10px; }
async function summarizePdfContent(viewer, apiModel, isConciseEnabled) { const searcher = viewer.searcher; try { // Fetch content from all pages in parallel for better performance const pageContentPromises = Array.from({ length: viewer.pageCount }, (_, i) => searcher.fetchPageContent(i)); const pageContents = await Promise.all(pageContentPromises); // Combine text from all pages into a single string const textContent = extractAnnotationsInfo(viewer) + pageContents.join(" ").trim(); // Check if there is any text content before calling analyzeTextContent if (textContent) { const summary = await analyzeTextContent(apiModel, textContent, isConciseEnabled); // Check if summary is a string if (typeof summary === "string") { return `Generated Summary:\n${summary}`; } // Otherwise, it's expected to be an object containing generated content const generatedText = summary?.choices?.[0]?.message?.content || "No content in the response."; // Convert timestamp to a readable date const createdDate = summary?.created ? new Date(summary.created * 1000).toLocaleString() // Convert Unix timestamp to readable format : "Creation date not available"; // Extract model used const modelUsed = summary?.model || "Model information not available"; // Return the generated summary with technical details and additional info return `Generated Summary:\n${generatedText}\n\nCreated: ${createdDate}\nModel: ${modelUsed}`; } else { return "[Error] The document does not contain any text content."; } } catch (error) { let errorMessage; if (typeof error === "string") { // If the error is a string, use it directly errorMessage = error; } else if (error instanceof Error) { // If the error is an instance of Error, use its message errorMessage = error.message; } else if (typeof error === "object" && error?.error?.message) { // If the error is an object with a nested error message errorMessage = error.error.message; } else { // Default fallback message errorMessage = "Server error. Unable to connect to ChatGPT server."; } return "[Error] " + errorMessage; } } function extractAnnotationsInfo(viewer) { try { // Select all sections within .pagescontent const sections = viewer.scrollView.querySelectorAll(".pagescontent section"); if(!sections.length) return ""; // Helper function to extract annotation type from class list function getAnnotationType(classList) { const annotationType = Array.from(classList).find(cls => cls.endsWith("Annotation")); return annotationType || "Undefined annotation"; } // Extract information for each section const annotations = Array.from(sections).map(section => { const classList = section.className.split(/\s+/); // Get all class names const annotationType = getAnnotationType(classList); // Extract annotation type const textContent = section.textContent.trim(); // Extract text content const pageElement = section.closest(".page"); // Find the closest parent with class "page" const pageIndex = pageElement ? pageElement.getAttribute("data-index") : "unknown"; // Extract page index return { annotationType, textContent, pageIndex }; }); // Generate a summary text const totalAnnotations = annotations.length; let summary = `The document contains ${totalAnnotations} annotations. Here is the list:\n`; summary += annotations.map(anno => `- ${anno.annotationType} on page ${anno.pageIndex + 1} contains text content: "${anno.textContent}"` ).join("\n"); return summary + "\n"; } catch (error) { return ""; } } // Utility function to map HTTP status codes to error names function getErrorNameByStatus(status) { const errorMap = { 100: "Continue", 101: "Switching Protocols", 200: "OK", 201: "Created", 202: "Accepted", 204: "No Content", 301: "Moved Permanently", 302: "Found", 304: "Not Modified", 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 408: "Request Timeout", 409: "Conflict", 413: "Payload Too Large", 415: "Unsupported Media Type", 429: "Too Many Requests", 500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", }; const message = errorMap[status] || "Unknown Error"; return `${message} (Status: ${status})`; } /** * Handles an error response and throws a detailed error based on the response status and body. * * @param {Response} response - The HTTP response object to check for errors. * @throws {Error} An error containing a user-friendly message based on the status code and response content. */ async function handleErrorResponse(response) { if (response.ok) return; let responseBody; try { // Attempt to read the response body as text responseBody = await response.text(); } catch { throw new Error('Failed to read the response body.'); } let errorToThrow; try { // Try to parse the response body as JSON const errorContent = JSON.parse(responseBody); const errorMessage = errorContent?.error?.message || JSON.stringify(errorContent); errorToThrow = new Error(`${getErrorNameByStatus(response.status)}. ${errorMessage}`); } catch (parseError) { // If parsing fails, use the raw text as the error message errorToThrow = new Error(`${getErrorNameByStatus(response.status)}. ${responseBody || ''}`); } throw errorToThrow; } // Method to analyze text content using the OpenAI summarize API async function analyzeTextContent(apiModel, content, isConciseEnabled) { console.log("analyzeTextContent:", content); const response = await fetch('api/openai/summarize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: apiModel, content: content, userLanguage: navigator.language || navigator.languages[0], isConciseEnabled: isConciseEnabled }), }); await handleErrorResponse(response); return response.json(); }
let public_setIsSummaryAdded; // Store the ID of the last created annotation for future removal let lastSummaryAnnotationId = null; var viewer; function addAiToolsPanel(viewerInst) { viewer = viewerInst; const React = viewer.getType('React'); // Create the AI tools panel with a custom icon and description, initially hidden and disabled const aiToolsPanelHandle = viewer.createPanel(createPanelContentElement(React, viewer), null, 'AiToolsPanel', { icon: { type: 'svg', content: createSvgIconElement(React) }, label: 'AI tools', description: 'AI tools panel', visible: false, enabled: false } ); // Add 'AiToolsPanel' to the layout of panels viewer.layoutPanels(['*', 'AiToolsPanel']); // Register an event to enable and expand the AI tools panel after a document opens viewer.onAfterOpen.register(function() { if(public_setIsSummaryAdded) { public_setIsSummaryAdded(false); public_setIsSummaryAdded = null; } resetSummary(true); viewer.updatePanel(aiToolsPanelHandle, { visible: true, enabled: true }); viewer.leftSidebar.menu.panels.open(aiToolsPanelHandle.id); viewer.leftSidebar.menu.panels.pin(aiToolsPanelHandle.id); }); } async function resetSummary(newDoc) { if (lastSummaryAnnotationId !== null) { await viewer.removeAnnotation(0, lastSummaryAnnotationId); lastSummaryAnnotationId = null; if(!newDoc) { await viewer.deletePage(0); // Remove the page where the summary was added } } } function createPanelContentElement(React, viewer) { // Define the panel content as a function component function PanelContentComponent(props) { // Local states for API model selection, API key input, button enablement, and loading state const [apiModel, setApiModel] = React.useState(() => { // Initialize the model from localStorage or use a default return localStorage.getItem('selectedGptApiModel') || 'gpt-4'; }); const [isButtonEnabled, setIsButtonEnabled] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false); const [isSummaryAdded, setIsSummaryAdded] = React.useState(false); // Track if summary is added const [isConciseEnabled, setIsConciseEnabled] = React.useState(true); // Enable the button if both API model and key inputs are populated and not loading React.useEffect(() => { setIsButtonEnabled(apiModel.trim().length > 0 && !isLoading); }, [apiModel, isLoading]); // Save the selected model to localStorage whenever it changes React.useEffect(() => { localStorage.setItem('selectedGptApiModel', apiModel); }, [apiModel]); // Handle changes to the selected AI model function handleModelChange(event) { setApiModel(event.target.value); } // Handle clicks on the "Summarize content" button async function handleSummarizeClick() { await handleResetSummaryClick(); setIsLoading(true); // Disable button during async operation await props.summarizePdfContent(apiModel, isConciseEnabled); setIsLoading(false); // Re-enable button after completion setIsSummaryAdded(true); // Mark summary as added public_setIsSummaryAdded = setIsSummaryAdded; } // Handle clicks on the "Reset Summary" button async function handleResetSummaryClick() { await resetSummary(false); setIsSummaryAdded(false); // Reset the state after resetting summary public_setIsSummaryAdded = null; } return React.createElement('div', { className: 'sample-ai-tools-panel', style: { margin: '20px' } }, // Dropdown for AI model selection React.createElement('label', { className: 'gc-label' }, 'Please select a model from the list to proceed:'), React.createElement('select', { value: apiModel, onChange: handleModelChange, className: 'gc-input' }, React.createElement('option', { value: 'gpt-3.5-turbo', title: 'GPT-3.5-Turbo: A fast and cost-efficient model suitable for general-purpose tasks.' }, 'GPT-3.5-Turbo'), React.createElement('option', { value: 'gpt-3.5-turbo-16k', title: 'GPT-3.5-Turbo 16k: Extended version of GPT-3.5-Turbo with a larger context window (16,000 tokens).' }, 'GPT-3.5-Turbo 16k'), React.createElement('option', { value: 'gpt-4', title: 'GPT-4: The most advanced OpenAI model, offering higher accuracy and reasoning capabilities.' }, 'GPT-4') ), React.createElement( 'label', { className: 'gc-toggle gc-toggle--block', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', }, title: 'Enable concise responses for shorter, more focused replies', }, React.createElement( 'span', { style: { margin: '0 auto', }, }, 'Concise responses' ), React.createElement('input', { type: 'checkbox', checked: isConciseEnabled, onChange: (e) => setIsConciseEnabled(e.target.checked), }) ), React.createElement('br'), // Button to summarize content, only enabled if both API model and key are provided and loading is false React.createElement('button', { onClick: handleSummarizeClick, disabled: !isButtonEnabled, className: 'gc-btn gc-btn--accent' }, isLoading ? 'Summarizing...' : 'Summarize content'), React.createElement('br'), React.createElement('br'), // Button to reset the summary, only visible if summary is added React.createElement('button', { onClick: handleResetSummaryClick, disabled: !isSummaryAdded, className: 'gc-btn' }, 'Reset Summary') ); } // Create the React component for panel content, including summarization function return React.createElement(PanelContentComponent, { summarizePdfContent: async (model, isConciseEnabled) => { // Call the PDF content summarization function let summaryResult = await summarizePdfContent(viewer, model, isConciseEnabled); const isError = summaryResult.startsWith("[Error]"); if (isError) { summaryResult = summaryResult.replace("[Error]", "").trim(); } const pageParams = { width: 612, height: 792, pageIndex: 0 }; await resetSummary(false); // Add an empty page to display the summary viewer.newPage(pageParams); // Create a new annotation with the summary result const summaryAnnotation = (await viewer.addAnnotation(0, { annotationType: 3, // AnnotationTypeCode.FREETEXT borderStyle: { width: isError ? 5 : 0, style: 1 }, color: [255, 255, 255], borderColor: isError ? [255, 0, 0] : [0, 255, 0], textAlignment: isError ? 1 : 0, // 0,1,2 - Left, Center, Right rect: [10, 10, pageParams.width - 20, pageParams.height - 20], isRichContents: false, fontSize: 16, contents: summaryResult })).annotation; // Store the annotation ID to remove it on the next summarization lastSummaryAnnotationId = summaryAnnotation.id; allowTextSelection(viewer.scrollView); } }); } function allowTextSelection(scrollArea) { const textDivs = scrollArea.querySelectorAll(".annotationLayer div.gc-text-content"); for (const div of textDivs) { // Allow text selection and remove interference div.setAttribute("contenteditable", true); div.style.userSelect = "text"; // Allow text selection div.style.outline = "none"; div.style.overflow = "auto"; div.style.pointerEvents = "auto"; // Ensure pointer events are enabled } // Ensure that parent elements do not block context menu or text selection scrollArea.addEventListener('contextmenu', (e) => { e.stopPropagation(); }, true); } function createSvgIconElement(React) { return React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "#ffffff" }, React.createElement("path", { d: "M6.005 7.5h12.74c1.253 0 2.255 1.007 2.255 2.249v5.252c0 1.242-1.010 2.249-2.255 2.249h-12.74c-1.253 0-2.255-1.007-2.255-2.249v-5.252c0-1.242 1.010-2.249 2.255-2.249zM5.996 8.25c-0.826 0-1.496 0.675-1.496 1.494v5.262c0 0.825 0.677 1.494 1.496 1.494h12.758c0.826 0 1.496-0.675 1.496-1.494v-5.262c0-0.825-0.677-1.494-1.496-1.494h-12.758zM14.25 10.5v3.75h-0.75v0.75h2.25v-0.75h-0.75v-3.75h0.75v-0.75h-2.25v0.75h0.75zM11.25 12.75h-2.25v2.25h-0.75v-3.75c0-0.834 0.673-1.5 1.504-1.5h0.743c0.833 0 1.504 0.672 1.504 1.5v3.75h-0.75v-2.25zM9.749 10.5c-0.414 0-0.749 0.333-0.749 0.75v0.75h2.25v-0.75c0-0.414-0.332-0.75-0.749-0.75h-0.752z" })); }