/** * 检测文本是否主要为中文 * 通过统计中文字符占比来判断 * @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} 提取的文本内容 */ 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} - 是否为中文文档 */ 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); 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 = ''; // dockToggleBtn.title = '展开'; // } // dockToggleBtn.onclick = function(event) { // event.preventDefault(); // const currentlyCollapsed = dock.classList.toggle('dock-collapsed'); // if (currentlyCollapsed) { // this.innerHTML = ''; // this.title = '展开'; // localStorage.setItem(dockCollapsedKey, 'true'); // } else { // this.innerHTML = ''; // 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')。 */