window.onload = async function() {
const viewer = new DsPdfViewer("#viewer", {
supportApi: getSupportApiSettings(),
restoreViewStateOnLoad: false,
language: "ja"
});
viewer.addDefaultPanels();
// ツールバーのボタンレイアウトをモードごとに設定します
viewer.toolbarLayout.viewer = {
default: ["open", "save", "$navigation", "$split", "$zoom", "$split", 'doc-title', "about"],
mobile: ["open", "save", "$navigation", "$split", "$zoom", "$split", 'doc-title', "about"],
fullscreen: ["$fullscreen", "open", "save", "$navigation", "$split", "$zoom", "$split", 'doc-title', "about"]
};
// ツールバーのレイアウトを適用します
viewer.applyToolbarLayout();
// AIツールパネルをビューアに追加します
addAiToolsPanel(viewer);
// PDFドキュメントを開きます
var pdfUrlToOpen = "/diodocs/pdfviewer/demos/product-bundles/assets/pdf/diodocs_a4_full.pdf";
await viewer.open(pdfUrlToOpen);
// ビューワの表示倍率を"ページ幅に合わせる"に設定します
viewer.zoomMode = 1;
}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>AIによるドキュメントの要約</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./src/styles.css">
<script src="/diodocs/pdfviewer/demos/product-bundles/build/dspdfviewer.js"></script>
<script src="/diodocs/pdfviewer/demos/product-bundles/build/wasmSupportApi.js"></script>
<script src="/diodocs/pdfviewer/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 {
// パフォーマンスを向上させるために、すべてのページから並列的にコンテンツを取得します
const pageContentPromises = Array.from({ length: viewer.pageCount }, (_, i) => searcher.fetchPageContent(i));
const pageContents = await Promise.all(pageContentPromises);
// 全ページのテキストを1つの文字列にまとめます
const textContent = extractAnnotationsInfo(viewer) + pageContents.join(" ").trim();
// analyzeTextContentを呼び出す前に、テキストコンテンツがあるかどうかをチェックします
if (textContent) {
const summary = await analyzeTextContent(apiModel, textContent, isConciseEnabled);
// 要約が文字列かどうかをチェックします
if (typeof summary === "string") {
return `Generated Summary:\n${summary}`;
}
// そうでない場合は、生成されたコンテンツを含むオブジェクトであると推測します
const generatedText = summary?.choices?.[0]?.message?.content || "No content in the response.";
// タイムスタンプを読み取り可能な日付に変換します
const createdDate = summary?.created
? new Date(summary.created * 1000).toLocaleString() // Convert Unix timestamp to readable format
: "Creation date not available";
// 使用したモデルを抽出します
const modelUsed = summary?.model || "Model information not available";
// 技術的な詳細と追加情報を含む生成結果(要約)を返します
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") {
// エラーが文字列形式の場合、そのまま使用します
errorMessage = error;
} else if (error instanceof Error) {
// エラーがインスタンスの場合、そのメッセージを使用します
errorMessage = error.message;
} else if (typeof error === "object" && error?.error?.message) {
// エラーがネストされたエラーメッセージを持つオブジェクトの場合、そのネストされたメッセージを使用します
errorMessage = error.error.message;
} else {
// その他の場合、固定メッセージを使用します
errorMessage = "Server error. Unable to connect to ChatGPT server.";
}
return "[Error] " + errorMessage;
}
}
function extractAnnotationsInfo(viewer) {
try {
// .pagescontent内のすべてのセクションを選択します
const sections = viewer.scrollView.querySelectorAll(".pagescontent section");
if(!sections.length)
return "";
// クラスリストから注釈タイプを抽出するヘルパー関数
function getAnnotationType(classList) {
const annotationType = Array.from(classList).find(cls => cls.endsWith("Annotation"));
return annotationType || "Undefined annotation";
}
// 各セクションの情報を抽出します
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
};
});
// 要約テキストを生成します
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 "";
}
}
// HTTPステータスコードとエラー名を対応付ける関数
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})`;
}
/**
* エラー・レスポンスを処理し、レスポンスのステータスとボディに基づいて詳細なエラーをスローする
*
* @param {Response} response - エラーをチェックするHTTPレスポンスオブジェクト
* @throws {Error} ステータスコードとレスポンスの内容に基づいた、分かりやすいメッセージを含むエラー
*/
async function handleErrorResponse(response) {
if (response.ok) return;
let responseBody;
try {
// レスポンス本文をテキストとして読み込みます
responseBody = await response.text();
} catch {
throw new Error('Failed to read the response body.');
}
let errorToThrow;
try {
// レスポンス・ボディをJSONとしてパースします
const errorContent = JSON.parse(responseBody);
const errorMessage = errorContent?.error?.message || JSON.stringify(errorContent);
errorToThrow = new Error(`${getErrorNameByStatus(response.status)}. ${errorMessage}`);
} catch (parseError) {
// パースに失敗した場合、原文をエラーメッセージとして使用します
errorToThrow = new Error(`${getErrorNameByStatus(response.status)}. ${responseBody || ''}`);
}
throw errorToThrow;
}
// 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');
// カスタムアイコンと説明を含むAIツールパネルを作成します(非表示・無効状態)
const aiToolsPanelHandle = viewer.createPanel(createPanelContentElement(React, viewer), null, 'AiToolsPanel',
{ icon: { type: 'svg', content: createSvgIconElement(React) },
label: 'AIツール', description: 'AIツールパネル',
visible: false, enabled: false } );
// 'AIツールパネル'をビューワに追加します
viewer.layoutPanels(['*', 'AiToolsPanel']);
// ドキュメントを開いた後に、AIツールパネルを有効にして展開するイベントを登録します
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) {
// "AIツールパネル"を生成します
function PanelContentComponent(props) {
// APIモデル選択、APIキー入力、ボタン有効化、ロード状態のローカル状態
const [apiModel, setApiModel] = React.useState(() => {
return localStorage.getItem('selectedGptApiModel') || 'gpt-4';
});
const [isButtonEnabled, setIsButtonEnabled] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
const [isSummaryAdded, setIsSummaryAdded] = React.useState(false);
const [isConciseEnabled, setIsConciseEnabled] = React.useState(true);
// APIモデルとキー入力の両方が入力され、ロードされていない場合、ボタンを有効にします
React.useEffect(() => {
setIsButtonEnabled(apiModel.trim().length > 0 && !isLoading);
}, [apiModel, isLoading]);
// 選択したモデルが変更されるたびにlocalStorageに保存します
React.useEffect(() => {
localStorage.setItem('selectedGptApiModel', apiModel);
}, [apiModel]);
// 選択したAIモデルの変更を処理します
function handleModelChange(event) {
setApiModel(event.target.value);
}
// "内容を要約"ボタンがクリックされた時の処理
async function handleSummarizeClick() {
await handleResetSummaryClick();
setIsLoading(true); // 非同期処理中、ボタンを無効化します
await props.summarizePdfContent(apiModel, isConciseEnabled);
setIsLoading(false); // 完了後、ボタンを有効化します
setIsSummaryAdded(true); // 要約が追加されたことをマークします
public_setIsSummaryAdded = setIsSummaryAdded;
}
// "要約をリセット"ボタンがクリックされた時の処理
async function handleResetSummaryClick() {
await props.resetSummary();
setIsSummaryAdded(false); // 要約をリセット後、状態をリセット
public_setIsSummaryAdded = null;
}
return React.createElement('div', { className: 'sample-ai-tools-panel', style: { margin: '20px' } },
// AIモデルを選択するためのドロップダウンリスト
React.createElement('label', { className: 'gc-label' }, 'リストからモデルを選択してください:'),
React.createElement('select', {
value: apiModel,
onChange: handleModelChange,
className: 'gc-input'
},
React.createElement('option', {
value: 'gpt-3.5-turbo',
title: 'GPT-3.5-Turbo: 汎用タスクに適した、高速でコスト効率の高いモデル。'
}, 'GPT-3.5-Turbo'),
React.createElement('option', {
value: 'gpt-3.5-turbo-16k',
title: 'GPT-3.5-Turbo 16k: コンテキストウィンドウが大きくなったGPT-3.5-Turboの拡張版(16,000トークン)。'
}, 'GPT-3.5-Turbo 16k'),
React.createElement('option', {
value: 'gpt-4',
title: 'GPT-4: OpenAIの最先端モデルで、より高い精度と推論能力を提供'
}, 'GPT-4')
),
React.createElement(
'label',
{
className: 'gc-toggle gc-toggle--block',
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
},
title: 'より短く、焦点を絞った要約とする',
},
React.createElement(
'span',
{
style: {
margin: '0 auto',
},
},
'簡潔な要約'
),
React.createElement('input', {
type: 'checkbox',
checked: isConciseEnabled,
onChange: (e) => setIsConciseEnabled(e.target.checked),
})
),
React.createElement('br'),
// コンテンツを要約するボタン:APIモデルとキーの両方が提供され、ローディングがfalseの場合のみ有効
React.createElement('button', {
onClick: handleSummarizeClick,
disabled: !isButtonEnabled,
className: 'gc-btn gc-btn--accent'
}, isLoading ? '要約中...' : '内容を要約'),
React.createElement('br'),
React.createElement('br'),
// 要約をリセットするボタン:要約が追加されている場合にのみ表示
React.createElement('button', {
onClick: handleResetSummaryClick,
disabled: !isSummaryAdded,
className: 'gc-btn'
}, '要約をリセット')
);
}
// 最後に作成された注釈のIDを保存し、削除できるようにしておきます
let lastSummaryAnnotationId = null;
// 要約機能を含む、パネル・コンテンツ用のReactコンポーネントを作成します
return React.createElement(PanelContentComponent, {
summarizePdfContent: async (model, isConciseEnabled) => {
// PDFの内容を要約する機能を呼び出します
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 };
// 既存の注釈がある場合は削除します
if (lastSummaryAnnotationId !== null) {
await viewer.removeAnnotation(0, lastSummaryAnnotationId);
} else {
// 要約を表示するために空のページを追加します
viewer.newPage(pageParams);
}
// 要約結果を含む注釈を生成します
const summaryAnnotation = (await viewer.addAnnotation(0, {
annotationType: 3, // フリーテキスト注釈
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 - 左, 中央, 右
rect: [10, 10, pageParams.width - 20, pageParams.height - 20],
isRichContents: false,
fontSize: 16,
contents: summaryResult
})).annotation;
// 注釈IDを保存し、次回の要約時に削除します
lastSummaryAnnotationId = summaryAnnotation.id;
allowTextSelection(viewer.scrollView);
},
resetSummary: async () => {
if (lastSummaryAnnotationId !== null) {
await viewer.removeAnnotation(0, lastSummaryAnnotationId);
lastSummaryAnnotationId = null;
}
//await viewer.deletePage(0); // 要約が追加されたページを削除します
}
});
}
function allowTextSelection(scrollArea) {
const textDivs = scrollArea.querySelectorAll(".annotationLayer div.gc-text-content");
for (const div of textDivs) {
// テキストの選択を許可し、干渉を除去する
div.setAttribute("contenteditable", true);
div.style.userSelect = "text";
div.style.outline = "none";
div.style.overflow = "auto";
div.style.pointerEvents = "auto";
}
// 親要素がコンテキストメニューやテキスト選択をブロックしないようにします
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"
}));
}