| |
| let sourceText = ""; |
| let targetText = ""; |
| let sourceFile = null; |
| let targetFile = null; |
| let sourceFileType = ""; |
| let targetFileType = ""; |
| let documentType = "regular"; |
| let analysis = null; |
| let correctedSourceText = ""; |
| let detectedCountry = ""; |
|
|
| |
| document.addEventListener("DOMContentLoaded", function() { |
| initializeApp(); |
| }); |
|
|
| |
| function initializeApp() { |
| |
| initDocumentTypeSelector(); |
| |
| |
| initFileUploadListeners(); |
| |
| |
| initTextAreaListeners(); |
| |
| |
| initButtonListeners(); |
| |
| |
| initDisplayOptions(); |
| |
| |
| initFilterOptions(); |
| } |
|
|
| |
| function initDocumentTypeSelector() { |
| |
| const selectorHtml = ` |
| <div class="document-type-selector mb-4"> |
| <p class="font-bold mb-2">نوع الملف:</p> |
| <div class="flex gap-4"> |
| <label class="flex items-center"> |
| <input type="radio" name="documentType" value="regular" checked class="mr-2"> |
| <span>ملف عادي</span> |
| </label> |
| <label class="flex items-center"> |
| <input type="radio" name="documentType" value="official" class="mr-2"> |
| <span>مستند رسمي</span> |
| </label> |
| </div> |
| </div> |
| `; |
| |
| |
| const sourceFileSection = document.querySelector("#sourceFileSection"); |
| sourceFileSection.insertAdjacentHTML('beforebegin', selectorHtml); |
| |
| |
| document.querySelectorAll('input[name="documentType"]').forEach(radio => { |
| radio.addEventListener('change', function() { |
| documentType = this.value; |
| console.log(`Document type changed to: ${documentType}`); |
| }); |
| }); |
| } |
|
|
| |
| function initFileUploadListeners() { |
| |
| const sourceFileInput = document.getElementById("sourceFileInput"); |
| if (sourceFileInput) { |
| sourceFileInput.addEventListener("change", function(e) { |
| handleFileUpload(e, "source"); |
| }); |
| } |
| |
| |
| const targetFileInput = document.getElementById("targetFileInput"); |
| if (targetFileInput) { |
| targetFileInput.addEventListener("change", function(e) { |
| handleFileUpload(e, "target"); |
| }); |
| } |
| |
| |
| initDragAndDropZones(); |
| } |
|
|
| |
| function initDragAndDropZones() { |
| const sourceDropZone = document.getElementById("sourceDropZone"); |
| const targetDropZone = document.getElementById("targetDropZone"); |
| |
| |
| if (sourceDropZone) { |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| sourceDropZone.addEventListener(eventName, preventDefaults, false); |
| }); |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| sourceDropZone.addEventListener(eventName, function() { |
| this.classList.add('bg-blue-100'); |
| }, false); |
| }); |
| |
| ['dragleave', 'drop'].forEach(eventName => { |
| sourceDropZone.addEventListener(eventName, function() { |
| this.classList.remove('bg-blue-100'); |
| }, false); |
| }); |
| |
| sourceDropZone.addEventListener('drop', function(e) { |
| const dt = e.dataTransfer; |
| if (dt.files.length) { |
| document.getElementById('sourceFileInput').files = dt.files; |
| handleFileUpload({ target: { files: dt.files } }, "source"); |
| } |
| }, false); |
| } |
| |
| |
| if (targetDropZone) { |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| targetDropZone.addEventListener(eventName, preventDefaults, false); |
| }); |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| targetDropZone.addEventListener(eventName, function() { |
| this.classList.add('bg-blue-100'); |
| }, false); |
| }); |
| |
| ['dragleave', 'drop'].forEach(eventName => { |
| targetDropZone.addEventListener(eventName, function() { |
| this.classList.remove('bg-blue-100'); |
| }, false); |
| }); |
| |
| targetDropZone.addEventListener('drop', function(e) { |
| const dt = e.dataTransfer; |
| if (dt.files.length) { |
| document.getElementById('targetFileInput').files = dt.files; |
| handleFileUpload({ target: { files: dt.files } }, "target"); |
| } |
| }, false); |
| } |
| } |
|
|
| |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
|
|
| |
| function initTextAreaListeners() { |
| const sourceTextArea = document.getElementById("sourceTextArea"); |
| const targetTextArea = document.getElementById("targetTextArea"); |
| |
| if (sourceTextArea) { |
| sourceTextArea.addEventListener("input", function() { |
| sourceText = this.value; |
| }); |
| } |
| |
| if (targetTextArea) { |
| targetTextArea.addEventListener("input", function() { |
| targetText = this.value; |
| }); |
| } |
| } |
|
|
| |
| function initButtonListeners() { |
| |
| const analyzeBtn = document.getElementById("analyzeBtn"); |
| if (analyzeBtn) { |
| analyzeBtn.addEventListener("click", startAnalysis); |
| } |
| |
| |
| const exportReportBtn = document.getElementById("exportReportBtn"); |
| if (exportReportBtn) { |
| exportReportBtn.addEventListener("click", exportReport); |
| } |
| |
| |
| const clearAllBtn = document.getElementById("clearAllBtn"); |
| if (clearAllBtn) { |
| clearAllBtn.addEventListener("click", clearAll); |
| } |
| } |
|
|
| |
| function initDisplayOptions() { |
| |
| const displayOptions = document.querySelectorAll('.display-option'); |
| displayOptions.forEach(option => { |
| if (option.dataset.display === 'classic') { |
| option.remove(); |
| } |
| }); |
| |
| |
| const splitViewOption = document.querySelector('.display-option[data-display="split"]'); |
| if (splitViewOption) { |
| splitViewOption.classList.add('active'); |
| setDisplayMode('split'); |
| } |
| |
| |
| document.querySelectorAll('.display-option').forEach(option => { |
| option.addEventListener('click', function() { |
| document.querySelectorAll('.display-option').forEach(opt => opt.classList.remove('active')); |
| this.classList.add('active'); |
| setDisplayMode(this.dataset.display); |
| }); |
| }); |
| } |
|
|
| |
| function initFilterOptions() { |
| document.querySelectorAll('.filter-option').forEach(option => { |
| option.addEventListener('click', function() { |
| this.classList.toggle('active'); |
| applyFilters(); |
| }); |
| }); |
| } |
|
|
| |
| function handleFileUpload(event, type) { |
| const file = event.target.files[0]; |
| if (!file) return; |
| |
| |
| if (type === "source") { |
| sourceFile = file; |
| document.getElementById("sourceFileName").textContent = file.name; |
| } else { |
| targetFile = file; |
| document.getElementById("targetFileName").textContent = file.name; |
| } |
| |
| |
| processFile(file, type); |
| } |
|
|
| |
| async function processFile(file, textType) { |
| const fileExtension = file.name.split('.').pop().toLowerCase(); |
| const fileType = file.type; |
| |
| |
| toggleLoading(true, textType); |
| |
| try { |
| let extractedText = ""; |
| |
| |
| if (fileType.includes('pdf') || fileExtension === 'pdf') { |
| extractedText = await processPDF(file); |
| } else if (fileType.includes('image') || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension)) { |
| extractedText = await processImage(file); |
| } else if (fileExtension === 'docx' || fileExtension === 'doc') { |
| extractedText = await processDocx(file); |
| } else if (['xlsx', 'xls', 'csv'].includes(fileExtension)) { |
| extractedText = await processExcel(file); |
| } else if (fileType.includes('text') || fileExtension === 'txt') { |
| extractedText = await processTextFile(file); |
| } else { |
| throw new Error("Unsupported file format. Please use PDF, image, Word, Excel, or text files."); |
| } |
| |
| |
| if (documentType === "official" && textType === "source") { |
| showNotification("جاري تصحيح الثوابت في المستند الرسمي...", "info"); |
| correctedSourceText = await correctOfficialDocument(extractedText); |
| |
| |
| extractedText = correctedSourceText; |
| showNotification("تم تصحيح الثوابت بنجاح", "success"); |
| } |
| |
| |
| if (textType === "source") { |
| sourceText = extractedText; |
| sourceFileType = fileExtension; |
| document.getElementById("sourceTextArea").value = extractedText; |
| } else { |
| targetText = extractedText; |
| targetFileType = fileExtension; |
| document.getElementById("targetTextArea").value = extractedText; |
| } |
| |
| |
| if (fileType.includes('pdf') || fileExtension === 'pdf') { |
| showPDFPreview(file, textType); |
| } |
| |
| |
| showOCRResults(extractedText, textType); |
| |
| |
| if (sourceText && targetText) { |
| document.getElementById("analyzeBtn").disabled = false; |
| } |
| |
| } catch (error) { |
| console.error("Error processing file:", error); |
| showNotification(`خطأ في معالجة الملف: ${error.message}`, "error"); |
| } finally { |
| |
| toggleLoading(false, textType); |
| } |
| } |
|
|
| |
| async function processPDF(file) { |
| return new Promise((resolve, reject) => { |
| const fileReader = new FileReader(); |
| |
| fileReader.onload = async function() { |
| try { |
| const typedArray = new Uint8Array(this.result); |
| |
| |
| const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise; |
| let text = ""; |
| |
| |
| for (let i = 1; i <= pdf.numPages; i++) { |
| const page = await pdf.getPage(i); |
| const textContent = await page.getTextContent(); |
| const pageText = textContent.items.map(item => item.str).join(' '); |
| text += pageText + "\n"; |
| } |
| |
| resolve(text); |
| } catch (error) { |
| console.error("Error extracting text from PDF:", error); |
| reject(error); |
| } |
| }; |
| |
| fileReader.onerror = function() { |
| reject(new Error("Error reading the PDF file.")); |
| }; |
| |
| fileReader.readAsArrayBuffer(file); |
| }); |
| } |
|
|
| |
| async function processImage(file) { |
| return new Promise((resolve, reject) => { |
| const fileReader = new FileReader(); |
| |
| fileReader.onload = async function() { |
| try { |
| |
| |
| showNotification("جاري استخراج النص من الصورة...", "info"); |
| |
| |
| |
| |
| await new Promise(res => setTimeout(res, 2000)); |
| |
| |
| |
| const ocrResult = "هذا نص تم استخراجه من الصورة باستخدام تقنية OCR.\nيمكنك استبدال هذا النص بنتائج OCR الفعلية من API الخاص بك."; |
| |
| resolve(ocrResult); |
| } catch (error) { |
| console.error("Error processing image:", error); |
| reject(error); |
| } |
| }; |
| |
| fileReader.onerror = function() { |
| reject(new Error("Error reading the image file.")); |
| }; |
| |
| fileReader.readAsDataURL(file); |
| }); |
| } |
|
|
| |
| async function processDocx(file) { |
| return new Promise((resolve, reject) => { |
| const fileReader = new FileReader(); |
| |
| fileReader.onload = async function() { |
| try { |
| |
| const arrayBuffer = this.result; |
| const result = await mammoth.extractRawText({ arrayBuffer }); |
| resolve(result.value); |
| } catch (error) { |
| console.error("Error extracting text from DOCX:", error); |
| reject(error); |
| } |
| }; |
| |
| fileReader.onerror = function() { |
| reject(new Error("Error reading the Word document.")); |
| }; |
| |
| fileReader.readAsArrayBuffer(file); |
| }); |
| } |
|
|
| |
| async function processExcel(file) { |
| return new Promise((resolve, reject) => { |
| const fileReader = new FileReader(); |
| |
| fileReader.onload = async function() { |
| try { |
| const data = new Uint8Array(this.result); |
| const workbook = XLSX.read(data, { type: 'array' }); |
| |
| |
| let text = ""; |
| workbook.SheetNames.forEach(sheetName => { |
| const worksheet = workbook.Sheets[sheetName]; |
| const sheetText = XLSX.utils.sheet_to_txt(worksheet); |
| text += `[Sheet: ${sheetName}]\n${sheetText}\n\n`; |
| }); |
| |
| resolve(text); |
| } catch (error) { |
| console.error("Error extracting text from Excel:", error); |
| reject(error); |
| } |
| }; |
| |
| fileReader.onerror = function() { |
| reject(new Error("Error reading the Excel file.")); |
| }; |
| |
| fileReader.readAsArrayBuffer(file); |
| }); |
| } |
|
|
| |
| async function processTextFile(file) { |
| return new Promise((resolve, reject) => { |
| const fileReader = new FileReader(); |
| |
| fileReader.onload = function() { |
| try { |
| resolve(this.result); |
| } catch (error) { |
| console.error("Error reading text file:", error); |
| reject(error); |
| } |
| }; |
| |
| fileReader.onerror = function() { |
| reject(new Error("Error reading the text file.")); |
| }; |
| |
| fileReader.readAsText(file); |
| }); |
| } |
|
|
| |
| async function correctOfficialDocument(text) { |
| try { |
| |
| detectedCountry = await detectCountryFromDocument(text); |
| console.log("Detected country:", detectedCountry); |
| |
| |
| const prompt = prepareFixedTermsPrompt(text, detectedCountry); |
| |
| |
| const correctionResponse = await callDeepSeekAPI(prompt, "correction"); |
| |
| |
| return extractCorrectedText(correctionResponse, text); |
| } catch (error) { |
| console.error("Error correcting official document:", error); |
| showNotification("حدث خطأ أثناء تصحيح المستند الرسمي", "error"); |
| return text; |
| } |
| } |
|
|
| |
| async function detectCountryFromDocument(text) { |
| try { |
| const prompt = ` |
| أنت خبير في تحليل المستندات الرسمية. قم بتحليل النص التالي وتحديد الدولة التي ينتمي إليها هذا المستند بناءً على المحتوى، الصياغة، المصطلحات المستخدمة، أو أي مؤشرات أخرى. |
| أعطني اسم الدولة فقط دون أي توضيح إضافي. |
| |
| النص: |
| ${text} |
| `; |
| |
| |
| const response = await callDeepSeekAPI(prompt, "country-detection"); |
| |
| |
| let country = response.trim(); |
| |
| |
| if (country.includes('\n')) { |
| country = country.split('\n')[0]; |
| } |
| |
| return country; |
| } catch (error) { |
| console.error("Error detecting country:", error); |
| return "غير محدد"; |
| } |
| } |
|
|
| |
| function prepareFixedTermsPrompt(text, country) { |
| return ` |
| أنت خبير في تصحيح المستندات الرسمية للدولة: ${country}. |
| |
| مهمتك هي تصحيح المصطلحات الثابتة فقط في المستند الرسمي التالي، مع الحفاظ على البيانات المتغيرة كما هي تماماً. |
| |
| قواعد التصحيح: |
| 1. صحح فقط المصطلحات والعبارات الثابتة مثل "الاسم"، "تاريخ الميلاد"، "الجنسية"، "الرقم القومي"، "رقم الهوية". |
| 2. لا تغير أبداً البيانات المتغيرة مثل أسماء الأشخاص، التواريخ، الأرقام، إلخ. |
| 3. حافظ على التنسيق الأصلي للنص. |
| 4. أعد النص كاملاً بعد التصحيح. |
| |
| على سبيل المثال: |
| - إذا كان النص يحتوي على "الأثم: محمد أحمد"، قم بتصحيحه إلى "الاسم: محمد أحمد" |
| - إذا كان النص يحتوي على "تاريخ الميلات: 1990/05/15"، قم بتصحيحه إلى "تاريخ الميلاد: 1990/05/15" |
| |
| النص المراد تصحيحه: |
| ${text} |
| |
| أعد النص كاملاً بعد التصحيح دون أي توضيحات إضافية. |
| `; |
| } |
|
|
| |
| function extractCorrectedText(response, originalText) { |
| |
| |
| |
| |
| return response; |
| } |
|
|
| |
| async function callDeepSeekAPI(prompt, type) { |
| |
| |
| |
| console.log(`Calling DeepSeek API for ${type} with prompt:`, prompt); |
| |
| |
| await new Promise(res => setTimeout(res, 3000)); |
| |
| |
| if (type === "correction") { |
| |
| return prompt.replace("الأثم:", "الاسم:") |
| .replace("تاريخ الميلات:", "تاريخ الميلاد:") |
| .replace("الجنسيه:", "الجنسية:") |
| .replace("رقم القومى:", "الرقم القومي:"); |
| } else if (type === "country-detection") { |
| |
| return "مصر"; |
| } else if (type === "analysis") { |
| |
| return { |
| differences: [ |
| { |
| sourceText: "هذا النص الأصلي يحتوي على خطأ.", |
| targetText: "هذا النص المترجم يحتوي على خطأ مختلف.", |
| errorType: "grammar", |
| explanation: "الجملة في النص المترجم تحتوي على خطأ نحوي.", |
| severity: "medium" |
| }, |
| |
| ], |
| summary: "تم العثور على 5 اختلافات بين النصين، منها 2 خطأ نحوي و3 أخطاء إملائية." |
| }; |
| } |
| |
| return "عذراً، حدث خطأ في معالجة طلبك."; |
| } |
|
|
| |
| async function startAnalysis() { |
| if (!sourceText || !targetText) { |
| showNotification("يرجى إدخال أو تحميل النصين المصدر والهدف", "error"); |
| return; |
| } |
| |
| try { |
| |
| toggleLoading(true, "analysis"); |
| showNotification("جاري تحليل النصوص...", "info"); |
| |
| |
| const analysisPrompt = ` |
| قم بتحليل النصين التاليين ومقارنتهما لتحديد الاختلافات والأخطاء: |
| |
| النص المصدر: |
| ${sourceText} |
| |
| النص الهدف: |
| ${targetText} |
| |
| قم بتحديد الاختلافات التالية: |
| 1. الأخطاء النحوية |
| 2. الأخطاء الإملائية |
| 3. الاختلافات في المعنى |
| 4. الكلمات أو العبارات المفقودة |
| 5. الإضافات غير الضرورية |
| |
| لكل اختلاف، قدم: |
| - النص المصدر |
| - النص الهدف |
| - نوع الخطأ |
| - شرح الخطأ |
| - مستوى خطورة الخطأ (منخفض، متوسط، مرتفع) |
| `; |
| |
| |
| const apiResponse = await callDeepSeekAPI(analysisPrompt, "analysis"); |
| |
| |
| analysis = apiResponse; |
| |
| |
| displayAnalysisResults(analysis); |
| |
| |
| document.getElementById("exportReportBtn").disabled = false; |
| |
| showNotification("تم الانتهاء من التحليل بنجاح", "success"); |
| } catch (error) { |
| console.error("Error during analysis:", error); |
| showNotification(`خطأ في التحليل: ${error.message}`, "error"); |
| } finally { |
| |
| toggleLoading(false, "analysis"); |
| } |
| } |
|
|
| |
| function displayAnalysisResults(analysis) { |
| const resultsContainer = document.getElementById("resultsContainer"); |
| resultsContainer.innerHTML = ""; |
| |
| |
| const summarySection = document.createElement("div"); |
| summarySection.className = "mb-6 p-4 bg-gray-50 rounded-lg"; |
| summarySection.innerHTML = ` |
| <h3 class="text-xl font-bold mb-2">ملخص التحليل</h3> |
| <p>${analysis.summary}</p> |
| `; |
| resultsContainer.appendChild(summarySection); |
| |
| |
| const differencesSection = document.createElement("div"); |
| differencesSection.className = "differences-section"; |
| |
| |
| const groupedDifferences = groupDifferencesByParagraph(analysis.differences); |
| |
| |
| let paragraphIndex = 1; |
| for (const [paragraph, differences] of Object.entries(groupedDifferences)) { |
| const paragraphSection = document.createElement("div"); |
| paragraphSection.className = "paragraph-section mb-8 p-4 bg-white rounded-lg shadow"; |
| |
| paragraphSection.innerHTML = ` |
| <h3 class="text-lg font-bold mb-4">فقرة ${paragraphIndex}</h3> |
| <div class="paragraph-content mb-4"> |
| <p class="whitespace-pre-wrap">${highlightDifferencesInParagraph(paragraph, differences)}</p> |
| </div> |
| <div class="paragraph-differences"> |
| <h4 class="font-bold mb-2">الاختلافات المكتشفة:</h4> |
| <ul class="differences-list"> |
| ${differences.map(diff => ` |
| <li class="difference-item mb-4 p-3 border-r-4 border-${getSeverityColor(diff.severity)} bg-${getSeverityColor(diff.severity)}-50"> |
| <div class="flex justify-between"> |
| <span class="font-bold">${getErrorTypeLabel(diff.errorType)}</span> |
| <span class="severity-badge bg-${getSeverityColor(diff.severity)}-200 text-${getSeverityColor(diff.severity)}-800 px-2 py-1 rounded text-sm"> |
| ${getSeverityLabel(diff.severity)} |
| </span> |
| </div> |
| <div class="mt-2"> |
| <div class="source-text mb-1"> |
| <span class="font-bold ml-1">النص الأصلي:</span> |
| <span class="text-gray-800">${diff.sourceText}</span> |
| </div> |
| <div class="target-text mb-1"> |
| <span class="font-bold ml-1">النص المترجم:</span> |
| <span class="text-gray-800">${diff.targetText}</span> |
| </div> |
| <div class="explanation mt-2 p-2 bg-gray-50 rounded"> |
| <span class="font-bold">الشرح:</span> |
| <p>${diff.explanation}</p> |
| </div> |
| </div> |
| </li> |
| `).join('')} |
| </ul> |
| </div> |
| `; |
| |
| differencesSection.appendChild(paragraphSection); |
| paragraphIndex++; |
| } |
| |
| resultsContainer.appendChild(differencesSection); |
| |
| |
| document.getElementById("resultsSection").classList.remove("hidden"); |
| |
| |
| addTooltipsToHighlights(); |
| } |
|
|
| |
| function groupDifferencesByParagraph(differences) { |
| const paragraphMap = {}; |
| |
| differences.forEach(diff => { |
| |
| const paragraph = extractParagraphFromText(diff.sourceText, sourceText); |
| |
| if (!paragraphMap[paragraph]) { |
| paragraphMap[paragraph] = []; |
| } |
| |
| paragraphMap[paragraph].push(diff); |
| }); |
| |
| return paragraphMap; |
| } |
|
|
| |
| function extractParagraphFromText(snippet, fullText) { |
| |
| const paragraphs = fullText.split(/\n\s*\n/); |
| |
| |
| for (const paragraph of paragraphs) { |
| if (paragraph.includes(snippet)) { |
| return paragraph; |
| } |
| } |
| |
| |
| return snippet; |
| } |
|
|
| |
| function highlightDifferencesInParagraph(paragraph, differences) { |
| let highlightedText = paragraph; |
| |
| |
| differences.sort((a, b) => { |
| const posA = paragraph.indexOf(a.sourceText); |
| const posB = paragraph.indexOf(b.sourceText); |
| return posA - posB; |
| }); |
| |
| |
| let offset = 0; |
| differences.forEach(diff => { |
| const diffId = `diff-${Math.random().toString(36).substr(2, 9)}`; |
| const originalText = diff.sourceText; |
| const startPos = paragraph.indexOf(originalText, offset); |
| |
| if (startPos !== -1) { |
| |
| const sentenceMatch = findSentenceContaining(paragraph, startPos, originalText.length); |
| |
| if (sentenceMatch) { |
| const { start, end } = sentenceMatch; |
| const sentence = paragraph.substring(start, end); |
| |
| |
| const highlightedSentence = `<span class="highlighted-sentence bg-${getSeverityColor(diff.severity)}-100 cursor-pointer" data-diff-id="${diffId}">${sentence}</span>`; |
| |
| |
| highlightedText = highlightedText.substring(0, start) + highlightedSentence + highlightedText.substring(end); |
| |
| |
| offset = end + (highlightedSentence.length - sentence.length); |
| |
| |
| window.diffDetails = window.diffDetails || {}; |
| window.diffDetails[diffId] = diff; |
| } |
| } |
| }); |
| |
| return highlightedText; |
| } |
|
|
| |
| function findSentenceContaining(text, position, length) { |
| |
| const sentenceEnders = ['. ', '? ', '! ', '.\n', '?\n', '!\n']; |
| |
| let start = position; |
| while (start > 0) { |
| let foundBoundary = false; |
| |
| for (const ender of sentenceEnders) { |
| const endPos = text.lastIndexOf(ender, start); |
| if (endPos !== -1 && endPos + ender.length <= start) { |
| start = endPos + ender.length; |
| foundBoundary = true; |
| break; |
| } |
| } |
| |
| if (foundBoundary || start === 0) break; |
| start--; |
| } |
| |
| let end = position + length; |
| while (end < text.length) { |
| let foundBoundary = false; |
| |
| for (const ender of sentenceEnders) { |
| const endPos = text.indexOf(ender, end - ender.length + 1); |
| if (endPos !== -1 && endPos >= end - ender.length + 1) { |
| end = endPos + ender.length; |
| foundBoundary = true; |
| break; |
| } |
| } |
| |
| if (foundBoundary || end === text.length) break; |
| end++; |
| } |
| |
| return { start, end }; |
| } |
|
|
| |
| function addTooltipsToHighlights() { |
| document.querySelectorAll('.highlighted-sentence').forEach(el => { |
| el.addEventListener('click', function() { |
| const diffId = this.dataset.diffId; |
| const diff = window.diffDetails[diffId]; |
| |
| if (diff) { |
| showDifferencePopup(diff, this); |
| } |
| }); |
| }); |
| } |
|
|
| |
| function showDifferencePopup(diff, element) { |
| |
| const existingPopup = document.getElementById('difference-popup'); |
| if (existingPopup) { |
| existingPopup.remove(); |
| } |
| |
| |
| const popup = document.createElement('div'); |
| popup.id = 'difference-popup'; |
| popup.className = 'fixed bg-white rounded-lg shadow-lg p-4 max-w-md z-50'; |
| popup.style.maxWidth = '90vw'; |
| |
| |
| popup.innerHTML = ` |
| <div class="flex justify-between items-center mb-2"> |
| <h3 class="font-bold text-lg">${getErrorTypeLabel(diff.errorType)}</h3> |
| <span class="severity-badge bg-${getSeverityColor(diff.severity)}-200 text-${getSeverityColor(diff.severity)}-800 px-2 py-1 rounded text-sm"> |
| ${getSeverityLabel(diff.severity)} |
| </span> |
| </div> |
| <div class="source-text mb-2"> |
| <span class="font-bold block mb-1">النص الأصلي:</span> |
| <p class="bg-gray-50 p-2 rounded">${diff.sourceText}</p> |
| </div> |
| <div class="target-text mb-2"> |
| <span class="font-bold block mb-1">النص المترجم:</span> |
| <p class="bg-gray-50 p-2 rounded">${diff.targetText}</p> |
| </div> |
| <div class="explanation mb-3"> |
| <span class="font-bold block mb-1">الشرح:</span> |
| <p class="bg-gray-50 p-2 rounded">${diff.explanation}</p> |
| </div> |
| <button id="close-popup" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-1 px-4 rounded"> |
| إغلاق |
| </button> |
| `; |
| |
| |
| document.body.appendChild(popup); |
| positionPopup(popup, element); |
| |
| |
| document.getElementById('close-popup').addEventListener('click', function() { |
| popup.remove(); |
| }); |
| |
| |
| document.addEventListener('click', function closePopup(e) { |
| if (!popup.contains(e.target) && e.target !== element) { |
| popup.remove(); |
| document.removeEventListener('click', closePopup); |
| } |
| }); |
| } |
|
|
| |
| function positionPopup(popup, element) { |
| const rect = element.getBoundingClientRect(); |
| const popupRect = popup.getBoundingClientRect(); |
| |
| |
| let top = rect.bottom + window.scrollY + 10; |
| let left = rect.left + window.scrollX; |
| |
| |
| if (top + popupRect.height > window.innerHeight + window.scrollY) { |
| |
| top = rect.top + window.scrollY - popupRect.height - 10; |
| } |
| |
| |
| if (left + popupRect.width > window.innerWidth) { |
| left = window.innerWidth - popupRect.width - 10; |
| } |
| |
| popup.style.top = `${top}px`; |
| popup.style.left = `${left}px`; |
| } |
|
|
| |
| function setDisplayMode(mode) { |
| const resultsContainer = document.getElementById("resultsContainer"); |
| |
| |
| resultsContainer.classList.remove('display-classic', 'display-split', 'display-side-by-side'); |
| |
| |
| resultsContainer.classList.add(`display-${mode}`); |
| } |
|
|
| |
| function applyFilters() { |
| const activeFilters = Array.from(document.querySelectorAll('.filter-option.active')) |
| .map(el => el.dataset.filter); |
| |
| |
| document.querySelectorAll('.difference-item').forEach(item => { |
| item.classList.remove('hidden'); |
| }); |
| |
| |
| if (activeFilters.length === 0) { |
| return; |
| } |
| |
| |
| document.querySelectorAll('.difference-item').forEach(item => { |
| const errorType = item.querySelector('.error-type').textContent; |
| const severity = item.querySelector('.severity-badge').textContent; |
| |
| const matchesFilter = activeFilters.some(filter => { |
| if (filter.startsWith('type-')) { |
| return errorType === filter.replace('type-', ''); |
| } else if (filter.startsWith('severity-')) { |
| return severity === filter.replace('severity-', ''); |
| } |
| return false; |
| }); |
| |
| if (!matchesFilter) { |
| item.classList.add('hidden'); |
| } |
| }); |
| } |
|
|
| |
| function exportReport() { |
| if (!analysis) { |
| showNotification("لا توجد نتائج تحليل للتصدير", "error"); |
| return; |
| } |
| |
| try { |
| |
| const reportContent = generateReportContent(); |
| |
| |
| const blob = new Blob([reportContent], { type: 'text/plain;charset=utf-8' }); |
| const url = URL.createObjectURL(blob); |
| |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = `تقرير_تحليل_الترجمة_${new Date().toISOString().slice(0, 10)}.txt`; |
| a.click(); |
| |
| URL.revokeObjectURL(url); |
| |
| showNotification("تم تصدير التقرير بنجاح", "success"); |
| } catch (error) { |
| console.error("Error exporting report:", error); |
| showNotification("حدث خطأ أثناء تصدير التقرير", "error"); |
| } |
| } |
|
|
| |
| function generateReportContent() { |
| let content = "تقرير تحليل الترجمة\n"; |
| content += "===================\n\n"; |
| |
| |
| content += `تاريخ التحليل: ${new Date().toLocaleString('ar-EG')}\n\n`; |
| |
| |
| content += "ملخص التحليل:\n"; |
| content += "-------------\n"; |
| content += analysis.summary + "\n\n"; |
| |
| |
| content += "تفاصيل الاختلافات:\n"; |
| content += "------------------\n\n"; |
| |
| analysis.differences.forEach((diff, index) => { |
| content += `${index + 1}. ${getErrorTypeLabel(diff.errorType)} (${getSeverityLabel(diff.severity)})\n`; |
| content += ` النص الأصلي: ${diff.sourceText}\n`; |
| content += ` النص المترجم: ${diff.targetText}\n`; |
| content += ` الشرح: ${diff.explanation}\n\n`; |
| }); |
| |
| |
| content += "تم إنشاء هذا التقرير بواسطة نظام تحليل الترجمة - شركة الريحان للترجمة"; |
| |
| return content; |
| } |
|
|
| |
| function clearAll() { |
| |
| document.getElementById("sourceTextArea").value = ""; |
| document.getElementById("targetTextArea").value = ""; |
| |
| |
| document.getElementById("sourceFileName").textContent = "لم يتم اختيار ملف"; |
| document.getElementById("targetFileName").textContent = "لم يتم اختيار ملف"; |
| |
| |
| document.getElementById("sourceOCRResults").innerHTML = ""; |
| document.getElementById("targetOCRResults").innerHTML = ""; |
| |
| |
| document.getElementById("resultsContainer").innerHTML = ""; |
| document.getElementById("resultsSection").classList.add("hidden"); |
| |
| |
| document.getElementById("sourcePDFPreview").innerHTML = ""; |
| document.getElementById("targetPDFPreview").innerHTML = ""; |
| document.getElementById("sourcePDFSection").classList.add("hidden"); |
| document.getElementById("targetPDFSection").classList.add("hidden"); |
| |
| |
| sourceText = ""; |
| targetText = ""; |
| sourceFile = null; |
| targetFile = null; |
| sourceFileType = ""; |
| targetFileType = ""; |
| analysis = null; |
| correctedSourceText = ""; |
| detectedCountry = ""; |
| |
| |
| document.querySelector('input[name="documentType"][value="regular"]').checked = true; |
| documentType = "regular"; |
| |
| |
| document.getElementById("analyzeBtn").disabled = true; |
| document.getElementById("exportReportBtn").disabled = true; |
| |
| showNotification("تم مسح جميع البيانات", "info"); |
| } |
|
|
| |
| function showPDFPreview(file, type) { |
| const previewSection = document.getElementById(`${type}PDFSection`); |
| const previewContainer = document.getElementById(`${type}PDFPreview`); |
| |
| |
| previewContainer.innerHTML = ""; |
| |
| |
| const viewer = document.createElement("div"); |
| viewer.className = "pdf-viewer"; |
| |
| |
| const embed = document.createElement("embed"); |
| embed.src = URL.createObjectURL(file); |
| embed.type = "application/pdf"; |
| embed.width = "100%"; |
| embed.height = "500px"; |
| |
| viewer.appendChild(embed); |
| previewContainer.appendChild(viewer); |
| |
| |
| previewSection.classList.remove("hidden"); |
| } |
|
|
| |
| function showOCRResults(text, type) { |
| const resultsContainer = document.getElementById(`${type}OCRResults`); |
| |
| |
| resultsContainer.innerHTML = ` |
| <div class="ocr-results p-3 bg-gray-50 rounded max-h-60 overflow-y-auto"> |
| <pre class="whitespace-pre-wrap text-sm">${text}</pre> |
| </div> |
| `; |
| } |
|
|
| |
| function showNotification(message, type = "info") { |
| |
| let notification = document.getElementById("notification"); |
| if (!notification) { |
| notification = document.createElement("div"); |
| notification.id = "notification"; |
| notification.className = "fixed bottom-4 right-4 p-4 rounded-lg shadow-lg transform transition-opacity duration-300 opacity-0"; |
| document.body.appendChild(notification); |
| } |
| |
| |
| notification.className = notification.className.replace(/bg-\w+-\d+/g, ""); |
| switch (type) { |
| case "success": |
| notification.classList.add("bg-green-500", "text-white"); |
| break; |
| case "error": |
| notification.classList.add("bg-red-500", "text-white"); |
| break; |
| case "warning": |
| notification.classList.add("bg-yellow-500", "text-white"); |
| break; |
| default: |
| notification.classList.add("bg-blue-500", "text-white"); |
| } |
| |
| |
| notification.textContent = message; |
| |
| |
| notification.classList.replace("opacity-0", "opacity-100"); |
| |
| |
| setTimeout(() => { |
| notification.classList.replace("opacity-100", "opacity-0"); |
| }, 3000); |
| } |
|
|
| |
| function toggleLoading(show, section) { |
| const loadingIndicators = { |
| "source": document.getElementById("sourceLoadingIndicator"), |
| "target": document.getElementById("targetLoadingIndicator"), |
| "analysis": document.getElementById("analysisLoadingIndicator") |
| }; |
| |
| const indicator = loadingIndicators[section]; |
| if (indicator) { |
| if (show) { |
| indicator.classList.remove("hidden"); |
| } else { |
| indicator.classList.add("hidden"); |
| } |
| } |
| } |
|
|
| |
| function getErrorTypeLabel(errorType) { |
| const labels = { |
| "grammar": "خطأ نحوي", |
| "spelling": "خطأ إملائي", |
| "meaning": "اختلاف في المعنى", |
| "missing": "نص مفقود", |
| "addition": "إضافة غير ضرورية", |
| "terminology": "مصطلح غير دقيق" |
| }; |
| |
| return labels[errorType] || errorType; |
| } |
|
|
| function getSeverityLabel(severity) { |
| const labels = { |
| "low": "منخفض", |
| "medium": "متوسط", |
| "high": "مرتفع" |
| }; |
| |
| return labels[severity] || severity; |
| } |
|
|
| function getSeverityColor(severity) { |
| const colors = { |
| "low": "yellow", |
| "medium": "orange", |
| "high": "red" |
| }; |
| |
| return colors[severity] || "gray"; |
| } |