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"
}));
}