import React, { useState, useRef, useEffect } from 'react'; import { Select, Button, Spin, Tooltip, Empty, Upload, message, Modal } from 'antd'; import { UploadOutlined, SoundOutlined, CopyOutlined, InfoCircleOutlined, GlobalOutlined, FileOutlined, FileImageOutlined, SyncOutlined, PauseOutlined, LoginOutlined } from '@ant-design/icons'; import { useTranslation } from '../js/i18n'; import { useNavigate, useLocation } from 'react-router-dom'; // Import PDF.js // Note: In a real implementation, you might want to properly set up PDF.js with its worker const pdfjsLib = window.pdfjsLib; if (pdfjsLib) { pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.min.js'; } // API configuration - reusing the same API as the Translator component const API_KEY = '28c81f920240b0fdbca940e07b86b8db'; const API_SECRET = 'd6e57784b134d09a8bed9ca004c98b4f'; const API_BASE_URL = 'https://www.heytransl.com'; const API_DOCUMENT_URL = `${API_BASE_URL}/api/translate/document`; /** * Generate authentication headers * @param {string} apiKey - API key * @param {string} apiSecret - API secret * @param {object} body - Request body * @returns {Promise} - Object containing authentication headers */ function generateHeaders(apiKey, apiSecret, body) { const timestamp = Math.floor(Date.now() / 1000).toString(); const bodyStr = JSON.stringify(body); const messageToSign = `${apiKey}${timestamp}${bodyStr}`; // Generate HMAC SHA-256 signature const encoder = new TextEncoder(); const key = encoder.encode(apiSecret); const message = encoder.encode(messageToSign); return crypto.subtle.importKey( 'raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ).then(key => { return crypto.subtle.sign( 'HMAC', key, message ); }).then(signature => { const hashArray = Array.from(new Uint8Array(signature)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return { 'X-API-Key': apiKey, 'X-Timestamp': timestamp, 'X-Signature': hashHex, 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }; }); } /** * Convert file to base64 string * @param {File} file - File object * @returns {Promise} - base64 string */ function fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result); reader.onerror = error => reject(error); }); } /** * Extract translation content from SSE response * @param {object} data - Parsed JSON object * @returns {string|null} - Extracted translation content or null */ function extractTranslationContent(data) { if (!data) return null; try { // Handle various response formats if (data.document_translation) { return data.document_translation; } if (data.translation_progress) { return data.translation_progress; } // For compatibility with existing extractTranslationContent function if (data.choices && data.choices.length > 0) { if (data.choices[0].delta && data.choices[0].delta.content !== undefined) { return data.choices[0].delta.content || ''; } if (data.choices[0].message && data.choices[0].message.content !== undefined) { return data.choices[0].message.content || ''; } if (data.choices[0].text !== undefined) { return data.choices[0].text || ''; } } // Handle other possible response formats if (data.translated_text !== undefined) { return data.translated_text || ''; } if (data.content !== undefined) { return data.content || ''; } if (data.text !== undefined) { return data.text || ''; } console.warn('Unknown translation data structure:', data); return ''; } catch (e) { console.error('Failed to extract translation content:', e, data); return ''; } } /** * Parse SSE event data * @param {string} data - SSE event data * @returns {object|null} - Parsed JSON object, or null */ function parseSSEData(data) { if (!data) return null; if (data === '[DONE]') return null; let jsonData = data; if (typeof data === 'string') { if (data.startsWith('data:')) { jsonData = data.substring(5).trim(); } if (jsonData === '[DONE]') return null; try { return JSON.parse(jsonData); } catch (e) { if (jsonData.trim()) { return { text: jsonData.trim() }; } return null; } } else if (typeof data === 'object') { return data; } return null; } /** * Process SSE stream * @param {Response} response - Fetch response object * @param {function} onData - Callback for processing each data chunk * @param {function} onDone - Callback for when processing is complete * @param {function} onError - Callback for errors */ async function processSSEStream(response, onData, onDone, onError) { if (!response.ok) { onError(new Error(`HTTP error: ${response.status}`)); return; } if (!response.body) { onError(new Error('Response has no readable data stream')); return; } try { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) { if (buffer.trim()) { const lines = buffer.split('\n'); for (const line of lines) { if (line.startsWith('data:')) { const data = line.substring(5).trim(); if (data === '[DONE]') { onDone(); return; } const parsedData = parseSSEData(data); if (parsedData) onData(parsedData); } } } break; } buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data:')) { const data = line.substring(5).trim(); if (data === '[DONE]') { onDone(); return; } const parsedData = parseSSEData(data); if (parsedData) onData(parsedData); } } } onDone(); } catch (error) { onError(error); } } const DocumentTranslatorContent = ({ currentLanguage, onLanguageChange }) => { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const [targetLanguage, setTargetLanguage] = useState(currentLanguage || '中文'); const [loading, setLoading] = useState(false); const [currentDocument, setCurrentDocument] = useState(null); const [pdfInstance, setPdfInstance] = useState(null); const [pdfScale, setPdfScale] = useState(1); const [documentData, setDocumentData] = useState(null); const [displayMode, setDisplayMode] = useState('translation'); // 'translation', 'bilingual', 'original' const [activeBlock, setActiveBlock] = useState(null); const pdfContentRef = useRef(null); const translationContentRef = useRef(null); const fileInputRef = useRef(null); const dropAreaRef = useRef(null); const abortControllerRef = useRef(null); const [isUserAuthenticated, setIsUserAuthenticated] = useState(false); // 检查用户登录状态的函数 const checkAuthentication = () => { const isAuth = localStorage.getItem('user') !== null; setIsUserAuthenticated(isAuth); return isAuth; }; // 组件挂载和路由变化时检查认证状态 useEffect(() => { checkAuthentication(); // 监听 storage 事件,处理其他标签页登录/登出的情况 const handleStorageChange = (e) => { if (e.key === 'user') { checkAuthentication(); } }; window.addEventListener('storage', handleStorageChange); return () => { window.removeEventListener('storage', handleStorageChange); }; }, [location]); // 检查并恢复待处理的文件上传 useEffect(() => { const checkPendingUpload = async () => { if (checkAuthentication()) { try { const pendingFileData = sessionStorage.getItem('pendingDocumentUpload'); if (pendingFileData) { console.log("找到待处理的文件上传:", pendingFileData); const fileData = JSON.parse(pendingFileData); // 从存储的数据创建 File 对象 const byteCharacters = atob(fileData.base64Data.split(',')[1]); const byteArrays = []; for (let offset = 0; offset < byteCharacters.length; offset += 512) { const slice = byteCharacters.slice(offset, offset + 512); const byteNumbers = new Array(slice.length); for (let i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); byteArrays.push(byteArray); } const blob = new Blob(byteArrays, { type: fileData.type }); const file = new File([blob], fileData.name, { type: fileData.type }); // 清除存储的数据 sessionStorage.removeItem('pendingDocumentUpload'); // 延迟一下再处理文件,确保组件已完全加载 setTimeout(() => { // 处理文件 handleDocumentFile(file); message.success(t('documentTranslator.continuePendingUpload')); }, 500); } } catch (error) { console.error('Error processing pending upload:', error); sessionStorage.removeItem('pendingDocumentUpload'); } } }; // 等待页面完全加载再检查 setTimeout(checkPendingUpload, 300); }, []); const languages = [ { value: '中文', label: '中文 (Chinese)' }, { value: 'English', label: 'English' }, { value: '日本語', label: '日本語 (Japanese)' }, { value: '한국어', label: '한국어 (Korean)' }, { value: 'Español', label: 'Español (Spanish)' }, { value: 'Français', label: 'Français (French)' }, { value: 'Deutsch', label: 'Deutsch (German)' }, { value: 'Русский', label: 'Русский (Russian)' }, { value: 'Português', label: 'Português (Portuguese)' }, { value: 'Italiano', label: 'Italiano (Italian)' }, { value: 'العربية', label: 'العربية (Arabic)' }, ]; // Update local state when props change useEffect(() => { if (currentLanguage && currentLanguage !== targetLanguage) { setTargetLanguage(currentLanguage); } }, [currentLanguage]); // Cleanup on component unmount useEffect(() => { return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); // Setup file drop event handlers useEffect(() => { // Clipboard paste handler const handlePaste = (e) => { if (!checkAuthentication()) { // For clipboard pastes, just redirect to login without saving window.location.href = '/login'; return; } if (e.clipboardData && e.clipboardData.items) { const items = e.clipboardData.items; for (const item of items) { if (item.type.indexOf('application/pdf') !== -1) { const file = item.getAsFile(); handleDocumentFile(file); message.success(t('documentTranslator.documentAddedFromClipboard')); break; } } } }; // Drag and drop handlers const handleDragOver = (e) => { e.preventDefault(); if (dropAreaRef.current) { dropAreaRef.current.classList.add('border-indigo-500'); } }; const handleDragLeave = (e) => { e.preventDefault(); if (dropAreaRef.current) { dropAreaRef.current.classList.remove('border-indigo-500'); } }; const handleDrop = (e) => { e.preventDefault(); if (dropAreaRef.current) { dropAreaRef.current.classList.remove('border-indigo-500'); } if (!checkAuthentication()) { if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { const file = e.dataTransfer.files[0]; if (file.type === 'application/pdf') { saveFileForLaterUpload(file); return; } } window.location.href = '/login'; return; } if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { const file = e.dataTransfer.files[0]; if (file.type !== 'application/pdf') { message.error(t('documentTranslator.uploadPdfOnly')); return; } handleDocumentFile(file); message.success(t('documentTranslator.documentAdded')); } }; document.addEventListener('paste', handlePaste); const dropArea = dropAreaRef.current; if (dropArea) { dropArea.addEventListener('dragover', handleDragOver); dropArea.addEventListener('dragleave', handleDragLeave); dropArea.addEventListener('drop', handleDrop); } return () => { document.removeEventListener('paste', handlePaste); if (dropArea) { dropArea.removeEventListener('dragover', handleDragOver); dropArea.removeEventListener('dragleave', handleDragLeave); dropArea.removeEventListener('drop', handleDrop); } }; }, [navigate]); // Handle window resize for PDF scale useEffect(() => { const handleResize = debounce(() => { if (currentDocument) { loadPDF(currentDocument.url); } }, 250); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [currentDocument]); // Helper function for debouncing function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } const saveFileForLaterUpload = async (file) => { if (!file) return; try { console.log("正在保存文件供登录后上传:", file.name); // 转换文件为 base64 const base64Data = await fileToBase64(file); // 保存文件信息到 sessionStorage const fileData = { name: file.name, type: file.type, base64Data }; // 使用JSON.stringify保存数据,确保正确序列化 sessionStorage.setItem('pendingDocumentUpload', JSON.stringify(fileData)); sessionStorage.setItem('redirectAfterLogin', '/document-translator'); // 跳转到登录页 // 使用window.location.href强制刷新而不是使用React Router的navigate window.location.href = '/login'; } catch (error) { console.error('Error saving file for later:', error); window.location.href = '/login'; } }; const handleDocumentFile = (file) => { if (!file) return; // 检查用户是否已登录 if (!checkAuthentication()) { console.log("用户未登录,保存文件并跳转到登录页"); saveFileForLaterUpload(file); return; } console.log("用户已登录,直接处理文件:", file.name); const reader = new FileReader(); reader.onload = (event) => { setCurrentDocument({ uid: Date.now(), name: file.name, status: 'done', url: event.target.result, originFileObj: file }); // 加载PDF并翻译 loadPDF(event.target.result); translateDocument(file, targetLanguage); }; reader.readAsDataURL(file); }; const translateDocument = async (file, language) => { if (!file) { message.warning(t('documentTranslator.uploadFirst')); return; } // 检查用户是否已登录 if (!checkAuthentication()) { console.log("翻译时发现用户未登录"); saveFileForLaterUpload(file); return; } // Abort previous request if any if (abortControllerRef.current) { abortControllerRef.current.abort(); } // Create new AbortController abortControllerRef.current = new AbortController(); const { signal } = abortControllerRef.current; setLoading(true); setDocumentData(null); try { // Convert document to base64 const base64Document = await fileToBase64(file); // Build request body const requestBody = { document_base64: base64Document, document_name: file.name, target_language: language || targetLanguage, stream: true }; // Generate base auth headers (API Key, Signature) const baseHeaders = await generateHeaders( API_KEY, API_SECRET, requestBody ); // Add user authentication token if available const finalHeaders = { ...baseHeaders }; const userDataString = localStorage.getItem('user'); if (userDataString) { try { const userData = JSON.parse(userDataString); if (userData && userData.token) { finalHeaders['Authorization'] = `Bearer ${userData.token}`; } else { console.warn('User token not found in localStorage data.'); } } catch (e) { console.error('Failed to parse user data from localStorage:', e); } } const response = await fetch(API_DOCUMENT_URL, { method: 'POST', headers: finalHeaders, // Use the combined headers body: JSON.stringify(requestBody), signal }); let translationResult = null; // Process SSE stream await processSSEStream( response, // Data callback (data) => { const content = extractTranslationContent(data); if (content) { if (typeof content === 'object') { // For structured translation data translationResult = content; setDocumentData(content); } else if (data.translation_progress) { // Handle progress updates if needed console.log('Translation progress:', data.translation_progress); } } }, // Complete callback () => { setLoading(false); if (translationResult) { setDocumentData(translationResult); } }, // Error callback (error) => { if (error.name !== 'AbortError') { console.error('Document translation error:', error); message.error(t('documentTranslator.serviceUnavailable')); // For development testing - use mock data if (process.env.NODE_ENV === 'development') { setTimeout(() => { // Mock data structure setDocumentData({ pages: [ { page_idx: 0, para_blocks: [ { type: 'title', text: 'Document Translation Example', translation: '文档翻译示例', bbox: [50, 50, 500, 100] }, { type: 'text', text: 'This is an example of document translation with immersive bilingual reading.', translation: '这是一个带有沉浸式双语阅读的文档翻译示例。', bbox: [50, 150, 500, 200] } ] } ] }); setLoading(false); }, 1500); } else { setLoading(false); } } else { setLoading(false); } } ); } catch (error) { if (error.name !== 'AbortError') { console.error('Document translation error:', error); message.error(t('documentTranslator.serviceUnavailable')); // For development testing if (process.env.NODE_ENV === 'development') { setTimeout(() => { // Add mock data here similar to above setLoading(false); }, 1500); } else { setLoading(false); } } else { setLoading(false); } } }; const loadPDF = async (url) => { if (!pdfjsLib) { message.error(t('documentTranslator.pdfJsNotLoaded')); return; } try { // Clear previous content if (pdfContentRef.current) { pdfContentRef.current.innerHTML = ''; } const loadingTask = pdfjsLib.getDocument(url); const pdf = await loadingTask.promise; setPdfInstance(pdf); const pdfContainer = pdfContentRef.current; if (!pdfContainer) return; for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { const page = await pdf.getPage(pageNum); const originalViewport = page.getViewport({ scale: 1 }); const containerWidth = pdfContainer.clientWidth - 40; const scale = containerWidth / originalViewport.width; setPdfScale(scale); const dpr = window.devicePixelRatio || 2; const scaledViewport = page.getViewport({ scale: scale * dpr }); const pageDiv = document.createElement('div'); pageDiv.className = 'pdf-page'; pageDiv.setAttribute('data-page-number', pageNum); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.height = scaledViewport.height; canvas.width = scaledViewport.width; canvas.style.width = `${scaledViewport.width / dpr}px`; canvas.style.height = `${scaledViewport.height / dpr}px`; const renderContext = { canvasContext: context, viewport: scaledViewport }; pageDiv.appendChild(canvas); pdfContainer.appendChild(pageDiv); await page.render(renderContext); } } catch (error) { console.error('Error loading PDF:', error); message.error(t('documentTranslator.pdfLoadFailed')); } }; const renderTranslations = () => { if (!documentData || !documentData.pages) return null; return documentData.pages.map((page, pageIndex) => { const pageNumber = page.page_idx + 1; return page.para_blocks.map((paraBlock, blockIndex) => { // Create paragraph block const paraDiv = (
{ setActiveBlock(`${pageNumber}-${blockIndex}`); highlightOriginalText(pageNumber, paraBlock.bbox); }} > {/* Original text block */} {(paraBlock.text || paraBlock.type === 'image' || paraBlock.type === 'table') && (
{renderParaBlock(paraBlock, 'original')}
)} {/* Translation block */} {(paraBlock.translation || paraBlock.type === 'image' || paraBlock.type === 'table') && (
{renderParaBlock(paraBlock, 'translation')}
)}
); return paraDiv; }); }); }; const renderParaBlock = (paraBlock, mode) => { const isOriginal = mode === 'original'; const content = isOriginal ? paraBlock.text : paraBlock.translation; switch (paraBlock.type) { case 'title': return

{content}

; case 'text': return

{content}

; case 'image': // Render image with caption return (
{paraBlock.image_url && {t('documentTranslator.imageAlt')}} {paraBlock.caption &&

{isOriginal ? paraBlock.caption : paraBlock.caption_translation}

}
); case 'table': // Render table with caption return (
{paraBlock.table_html &&
} {paraBlock.caption &&

{isOriginal ? paraBlock.caption : paraBlock.caption_translation}

}
); default: return

{content}

; } }; const highlightOriginalText = (pageNumber, bbox) => { // Remove previous highlights document.querySelectorAll('.highlighted').forEach(el => { el.parentElement.removeChild(el); }); // Find corresponding page const pageDiv = document.querySelector(`.pdf-page[data-page-number="${pageNumber}"]`); if (!pageDiv) return; // Create highlight element const [x1, y1, x2, y2] = bbox; const highlightEl = document.createElement('div'); highlightEl.className = 'highlighted'; highlightEl.style.position = 'absolute'; highlightEl.style.left = `${x1 * pdfScale}px`; highlightEl.style.top = `${y1 * pdfScale}px`; highlightEl.style.width = `${(x2 - x1) * pdfScale}px`; highlightEl.style.height = `${(y2 - y1) * pdfScale}px`; highlightEl.style.backgroundColor = 'yellow'; highlightEl.style.opacity = '0.5'; pageDiv.appendChild(highlightEl); // Scroll to highlight pageDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); }; const handleUpload = (info) => { if (!checkAuthentication()) { if (info.file && info.file.originFileObj) { saveFileForLaterUpload(info.file.originFileObj); } else { window.location.href = '/login'; } return; } const { file } = info; if (file.status !== 'uploading') { handleDocumentFile(file.originFileObj); } }; const removeDocument = () => { setCurrentDocument(null); setDocumentData(null); // Clear PDF container if (pdfContentRef.current) { pdfContentRef.current.innerHTML = ''; } // Clear translation container if (translationContentRef.current) { translationContentRef.current.innerHTML = ''; } }; const handleLanguageChange = (value) => { setTargetLanguage(value); // Propagate to parent component if needed if (onLanguageChange) { onLanguageChange(value); } // Retranslate if document exists if (currentDocument?.originFileObj && checkAuthentication()) { translateDocument(currentDocument.originFileObj, value); } }; const copyToClipboard = (text) => { navigator.clipboard.writeText(text).then(() => { message.success(t('documentTranslator.copied')); }, (err) => { message.error(t('documentTranslator.copyFailed')); console.error('Copy failed: ', err); }); }; return ( <>
{/* Header section */}
{t('translator.targetLanguageLabel')}
{currentDocument && (
{currentDocument.name} { if (!checkAuthentication()) { window.location.href = '/login'; e.target.value = ''; return; } if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; if (file.type === 'application/pdf') { handleDocumentFile(file); } else { message.error(t('documentTranslator.uploadPdfOnly')); } } e.target.value = ''; }} style={{ display: 'none' }} accept="application/pdf" />
)}
{/* Main content area */} {!currentDocument ? (

{t('translator.uploadFilePrompt')}

{ setTimeout(() => { onSuccess("ok"); }, 0); }} beforeUpload={(file) => { const isPdf = file.type === 'application/pdf'; if (!isPdf) { message.error(t('documentTranslator.uploadPdfOnly')); } return isPdf; }} onChange={handleUpload} > {!isUserAuthenticated && (

{t('translator.loginRequired')}

)}

{t('translator.supportsPdf')}

) : ( // Existing document viewer and translator UI
{/* PDF viewer side */}
{loading && !documentData && (

{t('documentTranslator.loading')}

)}
{/* Translation side */}
{loading && !documentData ? (

{t('documentTranslator.translating')}

) : documentData ? (
{renderTranslations()}
) : (
)}
)}
); }; export default DocumentTranslatorContent;