|
|
|
|
|
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"; |
|
|
} |