ChatGPT integration

This example demonstrates how to use ChatGPT for document summarization and content organization. The example sends text content to ChatGPT for analysis. If the text is too long, it splits it into smaller parts, sends each part to ChatGPT, and generates a brief summary of the content.

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, isConciseEnabled: isConciseEnabled }), }); await handleErrorResponse(response); return response.json(); }
let public_setIsSummaryAdded; function addAiToolsPanel(viewer) { 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; } viewer.updatePanel(aiToolsPanelHandle, { visible: true, enabled: true }); viewer.leftSidebar.menu.panels.open(aiToolsPanelHandle.id); viewer.leftSidebar.menu.panels.pin(aiToolsPanelHandle.id); }); } 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 props.resetSummary(); 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') ); } // Store the ID of the last created annotation for future removal let lastSummaryAnnotationId = null; // 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 }; // Remove the previous annotation if it exists if (lastSummaryAnnotationId !== null) { await viewer.removeAnnotation(0, lastSummaryAnnotationId); } else { // 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); }, resetSummary: async () => { if (lastSummaryAnnotationId !== null) { await viewer.removeAnnotation(0, lastSummaryAnnotationId); lastSummaryAnnotationId = null; } //await viewer.deletePage(0); // Remove the page where the summary was added } }); } 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" })); }