// history_detail_scripts.js - 从 history_detail.html 中提取的 JavaScript 代码 // 这个文件包含了历史详情页面的主要 JavaScript 逻辑 window.addEventListener('storage', function(e) { if (e.key === 'paperBurnerSettings') { // 重新加载设置并刷新 chatbot 配置 if (window.ChatbotCore && typeof window.ChatbotCore.getChatbotConfig === 'function') { // 你可以强制刷新 Chatbot UI 或重载配置 window.ChatbotUI && window.ChatbotUI.updateChatbotUI && window.ChatbotUI.updateChatbotUI(); } } }); /** * 将 exact 文本转为模糊正则,允许空格、换行模糊匹配,大小写不敏感 * @param {string} exact * @returns {RegExp} */ function escapeRegExp(string) { // 更安全地转义所有正则特殊字符 return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function fuzzyRegFromExact(exact) { // 先转义所有正则特殊字符 let pattern = escapeRegExp(exact); // 将所有空白替换为 \s+,允许跨行、多个空格 pattern = pattern.replace(/\s+/g, '\\s+'); // 可选:忽略前后空白 pattern = '\\s*' + pattern + '\\s*'; return new RegExp(pattern, 'gi'); } /** * 检查是否有OCR数据 * @returns {boolean} */ function hasOcrData() { return window.data && window.data.ocr && window.data.ocr.trim() !== ''; } /** * 检查是否有翻译数据 * @returns {boolean} */ function hasTranslationData() { return window.data && window.data.translation && window.data.translation.trim() !== ''; } /** * 检查是否有原始PDF数据 * @returns {boolean} */ function hasOriginalPdfData() { return window.data && window.data.metadata && window.data.metadata.originalPdfBase64; } /** * 将 Base64 字符串转换为 File 对象 * @param {string} base64 - Base64 编码的字符串(可带或不带 data: 前缀) * @param {string} filename - 文件名 * @returns {File} File 对象 */ function base64ToFile(base64, filename) { // 处理 data URL 格式 let dataUrl = base64; let mimeString = 'application/pdf'; if (base64.startsWith('data:')) { const matches = base64.match(/^data:([^;]+);base64,(.+)$/); if (matches) { mimeString = matches[1]; dataUrl = matches[2]; } } const byteString = atob(dataUrl); const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return new File([ab], filename || 'document.pdf', { type: mimeString }); } /** * 显示 Toast 消息 * @param {string} message - 消息内容 * @param {string} type - 类型: 'info', 'success', 'error', 'warning' * @param {number} duration - 显示时长(毫秒) */ function showToast(message, type = 'info', duration = 3000) { // 检查是否已有 toast 容器 let toastContainer = document.getElementById('pbx-toast-container'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.id = 'pbx-toast-container'; toastContainer.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 10001; display: flex; flex-direction: column; gap: 10px; `; document.body.appendChild(toastContainer); } // 创建 toast 元素 const toast = document.createElement('div'); const colors = { info: '#3b82f6', success: '#10b981', error: '#ef4444', warning: '#f59e0b' }; toast.style.cssText = ` padding: 12px 20px; background: ${colors[type] || colors.info}; color: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); font-size: 14px; max-width: 400px; word-wrap: break-word; animation: slideIn 0.3s ease-out; `; // 添加动画样式 if (!document.getElementById('pbx-toast-styles')) { const style = document.createElement('style'); style.id = 'pbx-toast-styles'; style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `; document.head.appendChild(style); } toast.textContent = message; toastContainer.appendChild(toast); // 自动移除 setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease-out forwards'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } // 如果容器为空,移除容器 if (toastContainer.children.length === 0) { toastContainer.parentNode.removeChild(toastContainer); } }, 300); }, duration); } /** * 执行 OCR 处理 * @param {File} file - 要处理的文件 * @param {Function} onProgress - 进度回调函数 (current, total, message) * @returns {Promise} OCR 结果 { markdown, images, metadata } */ async function performOcr(file, onProgress) { // 验证 OcrManager 可用 if (typeof OcrManager === 'undefined') { throw new Error('OCR 模块未加载,请刷新页面重试'); } // 创建 OcrManager 实例 const ocrManager = new OcrManager(); // 执行 OCR const result = await ocrManager.processFile(file, onProgress); return result; } /** * 执行翻译处理 * @param {string} markdown - OCR 后的 Markdown 文本 * @param {Function} onProgress - 进度回调函数 * @returns {Promise} 翻译后的文本 */ async function performTranslation(markdown, onProgress) { // 获取翻译设置 const settings = typeof loadSettings === 'function' ? loadSettings() : {}; const targetLang = settings.targetLanguage || 'zh-CN'; const targetLangName = targetLang === 'custom' ? (settings.customTargetLanguageName || '中文') : { 'zh-CN': '中文', 'en': 'English', 'ja': '日本語', 'ko': '한국어' }[targetLang] || targetLang; const selectedModel = settings.selectedTranslationModel || 'mistral'; // 获取 API Key let apiKey = ''; let modelConfig = null; if (selectedModel === 'custom') { // 自定义模型配置 modelConfig = settings.translationModelConfig || settings.customModelConfig || null; if (!modelConfig) { throw new Error('请先配置自定义翻译模型'); } } else { // 预设模型 - 从 Key 管理器获取 API Key if (typeof loadModelKeys === 'function') { const keys = loadModelKeys(selectedModel); const validKey = keys.find(k => k && k.value && (k.status === 'valid' || k.status === 'untested')); if (validKey) { apiKey = validKey.value.trim(); } } // 回退到 localStorage if (!apiKey) { const legacyKey = localStorage.getItem(`${selectedModel}ApiKeys`) || localStorage.getItem('translationApiKeys'); if (legacyKey) { apiKey = legacyKey.trim(); } } if (!apiKey) { throw new Error(`请先配置 ${selectedModel} 模型的 API Key`); } } // 分段翻译长文本 const chunks = splitMarkdownIntoChunks(markdown, 2000); let translatedText = ''; const totalChunks = chunks.length; onProgress && onProgress(0, totalChunks, '正在翻译...'); // 临时禁用 promptPoolUI,避免历史详情页访问不存在的 UI 元素 const originalPromptPoolUI = window.promptPoolUI; window.promptPoolUI = undefined; try { for (let i = 0; i < chunks.length; i++) { onProgress && onProgress(i + 1, totalChunks, `翻译中 (${i + 1}/${totalChunks})`); try { // 构建翻译选项 const translateOptions = {}; // 如果是自定义模型,需要传入 modelConfig if (selectedModel === 'custom' && modelConfig) { translateOptions.modelConfig = modelConfig; } // 调用 translateMarkdown 函数 - 不传 boundPrompt,让它使用内置提示词 const chunkResult = await translateMarkdown( chunks[i], targetLangName, selectedModel, apiKey, '[历史详情页翻译]', // logContext '', // defaultSystemPrompt - 空值会触发内置提示词 '', // defaultUserPromptTemplate - 空值会触发内置提示词 false, // useCustomPrompts true, // processTablePlaceholders translateOptions ); translatedText += chunkResult + '\n\n'; } catch (error) { console.error(`[performTranslation] 翻译第 ${i + 1} 块失败:`, error); // 如果某块翻译失败,保留原文 translatedText += chunks[i] + '\n\n'; } } } finally { // 恢复 promptPoolUI window.promptPoolUI = originalPromptPoolUI; } return translatedText.trim(); } /** * 将 Markdown 分割成小块 * @param {string} markdown - Markdown 文本 * @param {number} maxChars - 每块最大字符数 * @returns {string[]} 分割后的块数组 */ function splitMarkdownIntoChunks(markdown, maxChars = 2000) { const chunks = []; const lines = markdown.split('\n'); let currentChunk = ''; for (const line of lines) { if (currentChunk.length + line.length + 1 > maxChars) { if (currentChunk.trim()) { chunks.push(currentChunk.trim()); } currentChunk = line + '\n'; } else { currentChunk += line + '\n'; } } if (currentChunk.trim()) { chunks.push(currentChunk.trim()); } return chunks.length > 0 ? chunks : [markdown]; } /** * 创建确认对话框 * @param {string} title - 对话框标题 * @param {string} message - 对话框消息 * @param {string} confirmText - 确认按钮文本 * @param {string} cancelText - 取消按钮文本 * @returns {Promise} 用户是否确认 */ function showConfirmDialog(title, message, confirmText = '确认', cancelText = '取消') { return new Promise((resolve) => { // 创建对话框容器 const dialogOverlay = document.createElement('div'); dialogOverlay.id = 'pbx-confirm-dialog-overlay'; dialogOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; `; const dialogBox = document.createElement('div'); dialogBox.style.cssText = ` background: white; border-radius: 12px; padding: 24px; max-width: 400px; width: 90%; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); `; dialogBox.innerHTML = `

${title}

${message}

`; dialogOverlay.appendChild(dialogBox); document.body.appendChild(dialogOverlay); // 按钮事件 const confirmBtn = dialogBox.querySelector('#pbx-dialog-confirm'); const cancelBtn = dialogBox.querySelector('#pbx-dialog-cancel'); const cleanup = () => { document.body.removeChild(dialogOverlay); }; confirmBtn.onclick = () => { cleanup(); resolve(true); }; cancelBtn.onclick = () => { cleanup(); resolve(false); }; // 点击遮罩层关闭 dialogOverlay.onclick = (e) => { if (e.target === dialogOverlay) { cleanup(); resolve(false); } }; // ESC 键关闭 const handleEsc = (e) => { if (e.key === 'Escape') { cleanup(); resolve(false); document.removeEventListener('keydown', handleEsc); } }; document.addEventListener('keydown', handleEsc); }); } /** * 触发重新处理文档(OCR和/或翻译) * @param {boolean} includeTranslation - 是否包含翻译 */ async function triggerReprocess(includeTranslation) { const docId = window.docIdForLocalStorage; const docName = window.data ? window.data.name : '未知文档'; if (!docId) { showToast('无法获取文档ID,请刷新页面重试。', 'error'); return; } // 检查是否有原始 PDF 数据 const pdfBase64 = window.data?.metadata?.originalPdfBase64; if (!pdfBase64) { showToast('当前记录没有保存原始PDF数据,无法重新处理。', 'error'); return; } // 显示处理中 Toast showToast('正在准备文档...', 'info'); try { // 将 Base64 转为 File 对象 const file = base64ToFile(pdfBase64, docName); // 执行 OCR showToast('正在进行 OCR 识别...', 'info'); const ocrResult = await performOcr(file, (current, total, msg) => { showToast(`${msg || 'OCR 处理中'} (${current}/${total})`, 'info', 5000); }); // 更新 window.data window.data.ocr = ocrResult.markdown; if (ocrResult.images && ocrResult.images.length > 0) { window.data.images = ocrResult.images; } if (ocrResult.metadata) { window.data.metadata = window.data.metadata || {}; window.data.metadata.ocrEngine = ocrResult.metadata.engine; window.data.metadata.pageCount = ocrResult.metadata.pageCount; } // 如果需要翻译,执行翻译 if (includeTranslation) { showToast('正在进行翻译...', 'info'); try { const translationResult = await performTranslation(ocrResult.markdown, (current, total, msg) => { showToast(`${msg || '翻译中'} (${current}/${total})`, 'info', 5000); }); window.data.translation = translationResult; } catch (translationError) { console.error('[triggerReprocess] 翻译失败:', translationError); showToast(`翻译失败: ${translationError.message},但 OCR 已完成`, 'warning'); // 继续保存 OCR 结果,即使翻译失败 } } // 保存到 IndexedDB showToast('正在保存...', 'info'); await saveResultToDB(window.data); // 刷新页面显示 if (typeof renderDetail === 'function') { renderDetail(); } if (typeof showTab === 'function') { showTab(includeTranslation ? 'translation' : 'ocr'); } showToast('处理完成!', 'success'); } catch (error) { console.error('[triggerReprocess] 处理失败:', error); showToast(`处理失败: ${error.message}`, 'error'); } } /** * 处理"Word文档"标签点击 * 如果没有OCR数据,询问用户是否需要生成 */ async function handleOcrTabClick() { // 如果已有OCR数据,直接显示 if (hasOcrData()) { showTab('ocr'); return; } // 检查是否有原始PDF if (!hasOriginalPdfData()) { showToast('当前记录没有保存原始PDF数据,无法重新处理。', 'warning'); return; } // 弹出确认对话框 const confirmed = await showConfirmDialog( '生成Word文档', '当前文档尚未进行OCR处理。是否需要启动OCR处理生成Word文档?\n\n处理完成后可在本页面查看和导出。', '启动OCR', '取消' ); if (confirmed) { await triggerReprocess(false); // 仅OCR,不翻译 } } /** * 处理"仅翻译"标签点击 * 如果没有翻译数据,询问用户是否需要生成翻译对照文档 */ async function handleTranslationTabClick() { // 如果已有翻译数据,直接显示 if (hasTranslationData()) { showTab('translation'); return; } // 检查是否有原始PDF if (!hasOriginalPdfData()) { showToast('当前记录没有保存原始PDF数据,无法重新处理。', 'warning'); return; } // 弹出确认对话框 const confirmed = await showConfirmDialog( '生成翻译对照文档', '当前文档尚未进行翻译处理。是否需要启动OCR+翻译处理?\n\n这将生成原文Word文档和翻译对照文档,处理完成后可在本页面查看和导出。', '启动OCR+翻译', '取消' ); if (confirmed) { await triggerReprocess(true); // OCR + 翻译 } } // 绑定 tab 按钮点击事件 document.addEventListener('DOMContentLoaded', function() { if (document.getElementById('tab-ocr')) { document.getElementById('tab-ocr').onclick = handleOcrTabClick; } if (document.getElementById('tab-translation')) { document.getElementById('tab-translation').onclick = handleTranslationTabClick; } if (document.getElementById('tab-chunk-compare')) { document.getElementById('tab-chunk-compare').onclick = function() { showTab('chunk-compare'); }; } if (document.getElementById('tab-pdf-compare')) { document.getElementById('tab-pdf-compare').onclick = function() { showTab('pdf-compare'); }; } if (document.getElementById('tab-original-file')) { document.getElementById('tab-original-file').onclick = function() { showTab('original-file'); }; } // 页面加载后渲染详情 if (typeof renderDetail === 'function') { renderDetail(); } });