paper-burner/js/history/history_detail_render.js

458 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 检测文本是否主要为中文
* 通过统计中文字符占比来判断
* @param {string} text - 待检测的文本
* @returns {boolean} - 如果中文字符占比超过 30% 则认为是中文文档
*/
function isChineseDocument(text) {
if (!text || typeof text !== 'string') return false;
// 移除空白字符后进行检测
const cleanText = text.replace(/\s+/g, '');
if (cleanText.length === 0) return false;
// 匹配中文字符(包括简体和繁体)
const chineseCharRegex = /[\u4e00-\u9fff]/g;
const chineseChars = cleanText.match(chineseCharRegex);
if (!chineseChars) return false;
// 计算中文字符占比
const ratio = chineseChars.length / cleanText.length;
// 如果中文字符占比超过 30%,认为是中文文档
return ratio > 0.3;
}
/**
* 使用 PDF.js 从 PDF base64 数据中提取文本
* @param {string} base64Data - PDF 文件的 base64 编码数据
* @param {number} maxPages - 最大提取页数默认提取前3页用于检测
* @returns {Promise<string>} 提取的文本内容
*/
async function extractTextFromPdfBase64(base64Data, maxPages = 3) {
if (!base64Data || typeof pdfjsLib === 'undefined') {
return '';
}
try {
// 将 base64 转换为 Uint8Array
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// 使用 PDF.js 加载 PDF
const loadingTask = pdfjsLib.getDocument({ data: bytes });
const pdfDocument = await loadingTask.promise;
// 提取前几页的文本
const totalPages = Math.min(pdfDocument.numPages, maxPages);
let extractedText = '';
for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
const page = await pdfDocument.getPage(pageNum);
const textContent = await page.getTextContent();
const pageText = textContent.items.map(item => item.str).join(' ');
extractedText += pageText + ' ';
}
return extractedText;
} catch (error) {
console.error('[extractTextFromPdfBase64] 提取PDF文本失败:', error);
return '';
}
}
/**
* 根据文档语言隐藏不需要的 Tab
* 中文文档隐藏PDF对照、仅翻译、分块对比
* @param {Object} data - 文档数据对象
* @returns {Promise<boolean>} - 是否为中文文档
*/
async function hideTabsForChineseDocument(data) {
// 获取 OCR 文本用于判断语言
// 优先级contentListJson > ocr > ocrChunks > originalBinary(PDF提取)
let textToDetect = '';
const meta = data?.metadata?.metadata || data?.metadata || {};
// 1. 优先从 contentListJson 提取文本MinerU 结构化数据)
if (meta.contentListJson && Array.isArray(meta.contentListJson)) {
const textItems = meta.contentListJson
.filter(item => item.type === 'text' && item.text)
.map(item => item.text);
textToDetect = textItems.join(' ');
console.log('[hideTabsForChineseDocument] 从 contentListJson 提取文本,共', textItems.length, '个文本块');
}
// 2. 如果没有 contentListJson尝试 ocr 字段
if (!textToDetect && data.ocr && typeof data.ocr === 'string') {
textToDetect = data.ocr;
console.log('[hideTabsForChineseDocument] 使用 data.ocr 字段');
}
// 3. 尝试 metadata.ocr
if (!textToDetect && meta.ocr && typeof meta.ocr === 'string') {
textToDetect = meta.ocr;
console.log('[hideTabsForChineseDocument] 使用 metadata.ocr 字段');
}
// 4. 尝试 ocrChunks
if (!textToDetect && data.ocrChunks && Array.isArray(data.ocrChunks) && data.ocrChunks.length > 0) {
textToDetect = data.ocrChunks.map(chunk =>
typeof chunk === 'string' ? chunk : (chunk.text || chunk.content || '')
).join(' ');
console.log('[hideTabsForChineseDocument] 从 data.ocrChunks 提取文本');
}
// 5. 尝试 metadata.ocrChunks
if (!textToDetect && meta.ocrChunks && Array.isArray(meta.ocrChunks) && meta.ocrChunks.length > 0) {
textToDetect = meta.ocrChunks.map(chunk =>
typeof chunk === 'string' ? chunk : (chunk.text || chunk.content || '')
).join(' ');
console.log('[hideTabsForChineseDocument] 从 metadata.ocrChunks 提取文本');
}
// 6. 如果以上都没有尝试从原始PDF中提取文本
if (!textToDetect && meta.originalPdfBase64) {
console.log('[hideTabsForChineseDocument] 尝试从原始PDF提取文本进行语言检测...');
textToDetect = await extractTextFromPdfBase64(meta.originalPdfBase64, 3);
}
// 检测是否为中文文档
const isChinese = isChineseDocument(textToDetect);
console.log('[hideTabsForChineseDocument] 文档语言检测结果:', {
isChinese,
textLength: textToDetect.length,
textSample: textToDetect.substring(0, 100) + '...'
});
// 需要隐藏的 Tab ID 列表
const tabsToHide = [
'tab-pdf-compare', // PDF对照
'tab-translation', // 仅翻译
'tab-chunk-compare' // 分块对比
];
tabsToHide.forEach(tabId => {
const tabElement = document.getElementById(tabId);
if (tabElement) {
if (isChinese) {
tabElement.classList.add('hidden-tab');
console.log(`[hideTabsForChineseDocument] 隐藏 Tab: ${tabId}`);
} else {
tabElement.classList.remove('hidden-tab');
console.log(`[hideTabsForChineseDocument] 显示 Tab: ${tabId}`);
}
}
});
return isChinese;
}
/**
* 异步渲染历史详情页面的主函数。
* - 从 URL 查询参数中获取记录 ID。
* - 使用 `getResultFromDB` (来自 storage.js) 从 IndexedDB 加载对应的历史数据。
* - 如果数据成功加载:
* - 更新页面标题 (`#fileName`) 和元数据 (`#fileMeta`)。
* - 根据数据中是否存在有效的分块信息 (`ocrChunks`, `translatedChunks`)
* 决定默认显示的标签页(优先显示分块对比,否则显示 OCR 内容)。
* - 如果未找到数据,则显示提示信息。
* @async
*/
async function renderDetail() {
const id = getQueryParam('id');
if (!id) return;
docIdForLocalStorage = id; // Store doc ID for localStorage operations
window.docIdForLocalStorage = id; // 同时更新挂载到 window 对象上的变量
// Restore chatbot open state
const savedChatbotOpenState = localStorage.getItem(`chatbotOpenState_${docIdForLocalStorage}`);
if (savedChatbotOpenState === 'true') {
window.isChatbotOpen = true;
} else if (savedChatbotOpenState === 'false') {
window.isChatbotOpen = false;
}
// 从localStorage恢复保存的比例设置
const savedChunkCompareRatio = localStorage.getItem(`chunkCompareRatio_${docIdForLocalStorage}`);
if (savedChunkCompareRatio !== null && !isNaN(parseFloat(savedChunkCompareRatio))) {
window.chunkCompareRatio = parseFloat(savedChunkCompareRatio);
}
// console.log(`Chatbot state after attempting restore from localStorage for ${docIdForLocalStorage}: ${window.isChatbotOpen}`);
// Initialize Dock Logic once docIdForLocalStorage is available
if (typeof window.DockLogic !== 'undefined' && typeof window.DockLogic.init === 'function') {
window.DockLogic.init(docIdForLocalStorage);
} else {
console.error("DockLogic not available or init function missing.");
}
// 使用 storageAdapter 获取数据(支持后端模式)
let data;
if (window.storageAdapter && typeof window.storageAdapter.getResultFromDB === 'function') {
data = await window.storageAdapter.getResultFromDB(id);
} else if (typeof getResultFromDB === 'function') {
data = await getResultFromDB(id);
}
window.data = data; // for debugging
const fileMetaTimeEl = document.getElementById('fileMetaTime');
const fileMetaImagesEl = document.getElementById('fileMetaImages');
if (!data) {
document.getElementById('fileName').textContent = '未找到数据';
if (fileMetaTimeEl) fileMetaTimeEl.textContent = '时间: --';
if (fileMetaImagesEl) fileMetaImagesEl.textContent = '图片数: --';
document.getElementById('tabContent').innerHTML = '';
return;
}
// === 移除:按钮现在默认显示,不再动态控制显示/隐藏 ===
// 标签按钮原始文件、OCR、仅翻译、分块对比、PDF对照现在始终可见
// 仅保留数据检测用于默认标签选择
// 兼容嵌套的 metadata 结构
const meta = data?.metadata?.metadata || data?.metadata || {};
// 调试:打印后端数据结构
console.log('[renderDetail] 后端数据结构:', {
id: data.id,
name: data.name,
fileType: data.fileType,
hasMetadata: !!data.metadata,
metadataType: typeof data.metadata,
metadataKeys: data.metadata ? Object.keys(data.metadata) : [],
hasNestedMetadata: !!(data.metadata?.metadata),
nestedMetadataKeys: data.metadata?.metadata ? Object.keys(data.metadata.metadata) : [],
metaKeys: Object.keys(meta),
hasOriginalPdfBase64: !!meta.originalPdfBase64,
hasOcrChunks: !!(meta.ocrChunks?.length),
hasTranslatedChunks: !!(meta.translatedChunks?.length),
hasImages: !!(meta.images?.length),
hasContentListJson: !!meta.contentListJson,
hasTranslatedContentList: !!meta.translatedContentList
});
const hasMinerUStructuredData =
meta.originalPdfBase64 &&
meta.contentListJson &&
meta.translatedContentList &&
meta.supportsStructuredTranslation === true;
const hasOriginalPdf = !!(meta.originalPdfBase64);
// ========== 将 PDF base64 转换为 File 对象,供 AI 助手的 PDF 功能使用 ==========
if (hasOriginalPdf && data.name) {
try {
// 使用 history_detail_scripts.js 中定义的 base64ToFile 函数
if (typeof base64ToFile === 'function') {
window.currentOriginalFile = base64ToFile(meta.originalPdfBase64, data.name);
console.log('[renderDetail] 已设置 window.currentOriginalFile供 AI 助手使用');
} else {
// 如果 base64ToFile 不可用,手动转换
const base64Data = meta.originalPdfBase64;
const byteString = atob(base64Data);
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
window.currentOriginalFile = new File([ab], data.name, { type: 'application/pdf' });
console.log('[renderDetail] 已设置 window.currentOriginalFile手动转换供 AI 助手使用');
}
} catch (e) {
console.error('[renderDetail] 转换 PDF base64 为 File 失败:', e);
}
}
console.log('[renderDetail] 检测结果:', {
hasMinerUStructuredData,
hasOriginalPdf,
defaultTab: hasOriginalPdf ? 'original-file' : 'ocr'
});
document.getElementById('fileName').textContent = data.name;
if (fileMetaTimeEl) {
fileMetaTimeEl.textContent = `时间: ${new Date(data.time).toLocaleString()}`;
}
if (fileMetaImagesEl) {
const imageCount = Array.isArray(data.images) ? data.images.length : 0;
fileMetaImagesEl.textContent = `图片数: ${imageCount}`;
}
// ========== 确保批注数据在渲染前加载 ==========
if (id) { // 确保我们有文档 ID
try {
let annotations;
if (window.storageAdapter && typeof window.storageAdapter.getAnnotationsForDocFromDB === 'function') {
annotations = await window.storageAdapter.getAnnotationsForDocFromDB(id);
} else if (typeof getAnnotationsForDocFromDB === 'function') {
annotations = await getAnnotationsForDocFromDB(id);
}
console.log(`Annotations for docId '${id}' (loaded in renderDetail):`, annotations);
data.annotations = annotations || []; // 存储到 data 对象,确保是数组
// updateAnnotationSummary(); // Handled by updateAllDockStats via showTab
// updateHighlightSummary(); // Handled by updateAllDockStats via showTab
} catch (error) {
console.error(`Error loading annotations for docId '${id}' in renderDetail:`, error);
data.annotations = []; // 出错时也确保是个空数组
// updateAnnotationSummary(); // Handled by updateAllDockStats via showTab
// updateHighlightSummary(); // Handled by updateAllDockStats via showTab
}
} else {
// updateAnnotationSummary(); // Handled by updateAllDockStats via showTab
// updateHighlightSummary(); // Handled by updateAllDockStats via showTab
}
// =============================================
// ========== 在 window.data 设置并填充批注后,显式加载聊天记录 ==========
if (window.data) {
if (window.ChatbotCore && typeof window.ChatbotCore.reloadChatHistoryAndUpdateUI === 'function' &&
window.ChatbotUI && typeof window.ChatbotUI.updateChatbotUI === 'function') {
console.log('renderDetail: Calling reloadChatHistoryAndUpdateUI after window.data and annotations are set. Current docId:', window.ChatbotCore.getCurrentDocId ? window.ChatbotCore.getCurrentDocId() : 'unknown');
window.ChatbotCore.reloadChatHistoryAndUpdateUI(window.ChatbotUI.updateChatbotUI);
} else {
console.error('renderDetail: ChatbotCore or ChatbotUI not fully available for history reload.');
}
}
// =================================================================
// Initialize annotation system after data is loaded and DOM is likely ready
if (typeof window.initializeGlobalAnnotationVariables === 'function') {
window.initializeGlobalAnnotationVariables();
}
if (typeof window.initAnnotationSystem === 'function') {
window.initAnnotationSystem();
} else {
console.error("initAnnotationSystem is not defined. Check js/annotation_logic.js");
}
// ========== 检测文档语言并隐藏不必要的 Tab ==========
const isChinese = await hideTabsForChineseDocument(data);
// =============================================
// Determine initial tab, AFTER annotations are loaded
let initialTab = hasOriginalPdf ? 'original-file' : 'ocr'; // 有原始文件时默认显示原始文件
if (docIdForLocalStorage) {
const savedTabKey = `activeTab_${docIdForLocalStorage}`;
const savedTab = localStorage.getItem(savedTabKey);
if (
savedTab &&
['ocr', 'translation', 'chunk-compare', 'pdf-compare', 'original-file'].includes(savedTab) &&
!(savedTab !== 'ocr' && (!data.translation || data.translation.trim() === ""))
) {
initialTab = savedTab;
// 如果是中文文档,且保存的 Tab 是被隐藏的,则切换到默认 Tab
if (isChinese && ['translation', 'chunk-compare', 'pdf-compare'].includes(savedTab)) {
console.log(`[renderDetail] 中文文档,切换保存的 Tab 从 ${savedTab} 到 ocr`);
initialTab = hasOriginalPdf ? 'original-file' : 'ocr';
}
} else if (hasOriginalPdf) {
// 如果有原始文件,优先显示原始文件
initialTab = 'original-file';
} else if (
data.ocrChunks && data.ocrChunks.length > 0 &&
data.translatedChunks && data.translatedChunks.length > 0 &&
data.ocrChunks.length === data.translatedChunks.length &&
data.translation && data.translation.trim() !== ""
) {
initialTab = 'chunk-compare';
// 如果是中文文档,不显示分块对比
if (isChinese) {
initialTab = 'ocr';
}
}
} else if (hasOriginalPdf) {
// 如果有原始文件,优先显示原始文件
initialTab = 'original-file';
} else if (
data.ocrChunks && data.ocrChunks.length > 0 &&
data.translatedChunks && data.translatedChunks.length > 0 &&
data.ocrChunks.length === data.translatedChunks.length &&
data.translation && data.translation.trim() !== ""
) {
initialTab = 'chunk-compare';
// 如果是中文文档,不显示分块对比
if (isChinese) {
initialTab = 'ocr';
}
}
// 现在,在批注肯定加载完毕后,才调用 showTab
showTab(initialTab);
// The block for loading annotations (previously around line 415) has been moved up.
// Add scroll listener for saving scroll position (动态绑定到正确的滚动容器)
if (typeof bindScrollForSavePosition === 'function') {
bindScrollForSavePosition();
} else {
console.warn('[historyDetailRender] bindScrollForSavePosition function not found');
}
// Add scroll listener for updating reading progress - MOVED TO DOCK_LOGIC.JS
// window.removeEventListener('scroll', debouncedUpdateReadingProgress);
// window.addEventListener('scroll', debouncedUpdateReadingProgress);
// Add listener to save chatbot state on page unload
window.removeEventListener('beforeunload', saveChatbotStateOnUnload);
window.addEventListener('beforeunload', saveChatbotStateOnUnload);
// Manage Annotations Link Click - Changed to Settings Link - MOVED TO DOCK_LOGIC.JS
// const settingsLink = document.getElementById('settings-link');
// if (settingsLink) {
// settingsLink.onclick = function(event) {
// event.preventDefault();
// alert('管理页面即将推出!'); // Updated alert message
// };
// }
// Dock Toggle Button Click - MOVED TO DOCK_LOGIC.JS
// const dockToggleBtn = document.getElementById('dock-toggle-btn');
// const dock = document.getElementById('bottom-left-dock');
// if (dockToggleBtn && dock) {
// // Restore collapsed state
// const dockCollapsedKey = `dockCollapsed_${docIdForLocalStorage}`;
// const isCollapsed = localStorage.getItem(dockCollapsedKey) === 'true';
// if (isCollapsed) {
// dock.classList.add('dock-collapsed');
// dockToggleBtn.innerHTML = '<i class="fa fa-chevron-up"></i>';
// dockToggleBtn.title = '展开';
// }
// dockToggleBtn.onclick = function(event) {
// event.preventDefault();
// const currentlyCollapsed = dock.classList.toggle('dock-collapsed');
// if (currentlyCollapsed) {
// this.innerHTML = '<i class="fa fa-chevron-up"></i>';
// this.title = '展开';
// localStorage.setItem(dockCollapsedKey, 'true');
// } else {
// this.innerHTML = '<i class="fa fa-chevron-down"></i>';
// this.title = '折叠';
// localStorage.setItem(dockCollapsedKey, 'false');
// }
// };
// }
}
/**
* 切换并显示指定的标签页内容。
* - 更新标签按钮的激活状态 (`active` class)。
* - 根据传入的 `tab` 参数 ( 'ocr', 'translation', 'chunk-compare' ),生成对应的 HTML 内容。
* - OCR 和翻译标签页:直接渲染 `data.ocr` 或 `data.translation` 字段。
* - 分块对比标签页 (`chunk-compare`)
* - 检查 `data.ocrChunks` 和 `data.translatedChunks` 是否有效且数量匹配。
* - 如果有效,则为每一对原文/译文块生成对比视图。使用 `renderLevelAlignedFlex` 进行布局。
* - 提供一个按钮 (`#swap-chunks-btn`) 用于切换原文和译文在对比视图中的左右位置。
* - 如果分块数据无效,则显示提示信息。
* - 将生成的 HTML 设置到 `#tabContent` 区域。
* - 调用 `window.refreshTocList()` 更新目录TOC
*
* @param {string} tab - 要显示的标签页标识符 ('ocr', 'translation', or 'chunk-compare')。
*/