|
<!DOCTYPE html> |
|
<html lang="ar" dir="rtl"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>نظام المقارنة والترجمة المطور - شركة الريحان للترجمة</title> |
|
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet"> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script> |
|
<style> |
|
|
|
|
|
|
|
@keyframes gradient { |
|
0% { background-position: 0% 50%; } |
|
50% { background-position: 100% 50%; } |
|
100% { background-position: 0% 50%; } |
|
} |
|
.animate-gradient { |
|
background-size: 200% 200%; |
|
animation: gradient 15s ease infinite; |
|
} |
|
.transition-all { transition: all 0.3s ease-in-out; } |
|
.animate-scale { transition: transform 0.2s ease-in-out; } |
|
.animate-scale:hover { transform: scale(1.02); } |
|
.pulse-animation { animation: pulse 2s infinite; } |
|
@keyframes pulse { |
|
0% { box-shadow: 0 0 0 0 rgba(156,39,176,0.4); } |
|
70% { box-shadow: 0 0 0 10px rgba(156,39,176,0); } |
|
100% { box-shadow: 0 0 0 0 rgba(156,39,176,0); } |
|
} |
|
|
|
|
|
|
|
|
|
.text-comparison { line-height: 1.8; white-space: pre-wrap; } |
|
.highlight-number { |
|
background-color: #FDE68A; |
|
padding: 0 4px; |
|
border-radius: 3px; |
|
font-weight: bold; |
|
} |
|
.highlight-missing { |
|
background-color: #BFDBFE; |
|
padding: 0 4px; |
|
border-radius: 3px; |
|
font-style: italic; |
|
} |
|
.highlight-meaning { |
|
background-color: #fecaca; |
|
color: #B91C1C; |
|
padding: 0 4px; |
|
border-radius: 3px; |
|
font-weight: bold; |
|
} |
|
|
|
|
|
|
|
|
|
.split-view { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); |
|
gap: 1rem; |
|
} |
|
.line-item { |
|
display: flex; |
|
align-items: flex-start; |
|
margin-bottom: 0.5rem; |
|
} |
|
.line-number { |
|
width: 30px; |
|
font-weight: bold; |
|
color: #4B5563; |
|
flex-shrink: 0; |
|
} |
|
.line-text { flex: 1; } |
|
|
|
|
|
|
|
|
|
.card-hover { |
|
transition: all 0.3s ease; |
|
} |
|
.card-hover:hover { |
|
box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.1), 0 10px 10px -5px rgba(59, 130, 246, 0.04); |
|
transform: translateY(-2px); |
|
} |
|
|
|
|
|
|
|
|
|
.collapsible-section { |
|
border: 1px solid #e5e7eb; |
|
border-radius: 8px; |
|
margin-bottom: 8px; |
|
overflow: hidden; |
|
background-color: #f9fafb; |
|
} |
|
|
|
.section-header { |
|
background-color: #f3f4f6; |
|
padding: 12px 16px; |
|
cursor: pointer; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
font-weight: 600; |
|
border-bottom: 1px solid #e5e7eb; |
|
} |
|
|
|
.section-header:hover { |
|
background-color: #e5e7eb; |
|
} |
|
|
|
.section-content { |
|
max-height: 0; |
|
overflow: hidden; |
|
transition: max-height 0.3s ease-out; |
|
padding: 0 16px; |
|
} |
|
|
|
.section-content.open { |
|
max-height: 1000px; |
|
padding: 16px; |
|
transition: max-height 0.5s ease-in, padding 0.3s ease-in; |
|
} |
|
|
|
.draft-marker { |
|
display: inline-block; |
|
background-color: #e5e7eb; |
|
padding: 2px 6px; |
|
border-radius: 4px; |
|
font-size: 0.75rem; |
|
margin-right: 8px; |
|
color: #4b5563; |
|
} |
|
|
|
.sync-word { |
|
color: #4f46e5; |
|
font-weight: 500; |
|
} |
|
|
|
.paragraph-section { |
|
border-left: 3px solid #d1d5db; |
|
padding-left: 12px; |
|
margin-bottom: 16px; |
|
} |
|
|
|
|
|
|
|
|
|
.logo-container { |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.logo { |
|
width: 80px; |
|
height: 80px; |
|
border-radius: 50%; |
|
background-color: #fff; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
margin-left: 1rem; |
|
overflow: hidden; |
|
} |
|
|
|
.company-name { |
|
font-size: 1.5rem; |
|
font-weight: bold; |
|
color: #fff; |
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2); |
|
} |
|
|
|
|
|
.segment-comparison { |
|
border: 1px solid #e5e7eb; |
|
border-radius: 8px; |
|
margin-bottom: 1rem; |
|
overflow: hidden; |
|
} |
|
|
|
.segment-header { |
|
background-color: #f3f4f6; |
|
padding: 0.75rem 1rem; |
|
font-weight: 600; |
|
border-bottom: 1px solid #e5e7eb; |
|
display: flex; |
|
justify-content: space-between; |
|
} |
|
|
|
.segment-content { |
|
display: grid; |
|
grid-template-columns: 1fr 1fr; |
|
gap: 1px; |
|
background-color: #e5e7eb; |
|
} |
|
|
|
.segment-source, .segment-target { |
|
background-color: #fff; |
|
padding: 1rem; |
|
} |
|
|
|
.segment-source { |
|
background-color: #f0f9ff; |
|
} |
|
|
|
.segment-target { |
|
background-color: #fdf2f8; |
|
} |
|
|
|
.segment-notes { |
|
grid-column: span 2; |
|
background-color: #fffbeb; |
|
padding: 0.75rem 1rem; |
|
border-top: 1px solid #e5e7eb; |
|
font-size: 0.9rem; |
|
} |
|
|
|
.segment-tag { |
|
display: inline-block; |
|
padding: 0.25rem 0.5rem; |
|
border-radius: 9999px; |
|
font-size: 0.75rem; |
|
font-weight: 500; |
|
margin-right: 0.5rem; |
|
} |
|
|
|
.tag-error { |
|
background-color: #fee2e2; |
|
color: #b91c1c; |
|
} |
|
|
|
.tag-warning { |
|
background-color: #fef3c7; |
|
color: #92400e; |
|
} |
|
|
|
.tag-info { |
|
background-color: #dbeafe; |
|
color: #1e40af; |
|
} |
|
|
|
.view-tabs { |
|
display: flex; |
|
border-bottom: 1px solid #e5e7eb; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.view-tab { |
|
padding: 0.75rem 1.5rem; |
|
cursor: pointer; |
|
border-bottom: 2px solid transparent; |
|
font-weight: 500; |
|
} |
|
|
|
.view-tab.active { |
|
border-bottom-color: #3b82f6; |
|
color: #3b82f6; |
|
} |
|
|
|
.view-content { |
|
display: none; |
|
} |
|
|
|
.view-content.active { |
|
display: block; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
.pdf-grid { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); |
|
gap: 10px; |
|
margin-top: 15px; |
|
} |
|
|
|
.pdf-page { |
|
border: 1px solid #ddd; |
|
border-radius: 5px; |
|
padding: 5px; |
|
position: relative; |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
} |
|
|
|
.pdf-page:hover { |
|
transform: translateY(-3px); |
|
box-shadow: 0 3px 10px rgba(0,0,0,0.1); |
|
} |
|
|
|
.pdf-page.selected { |
|
border: 2px solid #3b82f6; |
|
} |
|
|
|
.pdf-page img { |
|
width: 100%; |
|
height: auto; |
|
border-radius: 3px; |
|
} |
|
|
|
.page-number { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
right: 0; |
|
background-color: rgba(0,0,0,0.6); |
|
color: white; |
|
text-align: center; |
|
font-size: 12px; |
|
padding: 2px; |
|
} |
|
|
|
|
|
.result-text { |
|
max-height: 300px; |
|
overflow-y: auto; |
|
white-space: pre-wrap; |
|
direction: rtl; |
|
border: 1px solid #ddd; |
|
padding: 10px; |
|
border-radius: 5px; |
|
background-color: #f8f9fa; |
|
} |
|
|
|
|
|
.page-preview { |
|
margin-bottom: 10px; |
|
border: 1px solid #ddd; |
|
border-radius: 5px; |
|
padding: 10px; |
|
} |
|
|
|
.page-preview h4 { |
|
background-color: #f0f8ff; |
|
padding: 5px; |
|
border-radius: 3px; |
|
margin-bottom: 10px; |
|
} |
|
|
|
|
|
.progress { |
|
height: 0.5rem; |
|
border-radius: 9999px; |
|
overflow: hidden; |
|
background-color: #e5e7eb; |
|
} |
|
|
|
.progress-bar { |
|
height: 100%; |
|
border-radius: 9999px; |
|
background-color: #3b82f6; |
|
transition: width 0.3s ease; |
|
} |
|
|
|
|
|
.stats-badge { |
|
background-color: #3b82f6; |
|
color: white; |
|
font-size: 14px; |
|
padding: 5px 10px; |
|
border-radius: 20px; |
|
margin-right: 10px; |
|
} |
|
|
|
.stats-container { |
|
display: flex; |
|
align-items: center; |
|
margin-bottom: 15px; |
|
} |
|
|
|
|
|
.bg-white-classic { |
|
background-color: #ffffff; |
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-white"> |
|
<div class="min-h-screen pb-12"> |
|
|
|
<header class="bg-gradient-to-r from-blue-600 to-blue-800 text-white py-10 mb-10 shadow-xl"> |
|
<div class="max-w-6xl mx-auto px-4"> |
|
|
|
<div class="flex items-center justify-center mb-6"> |
|
<div class="logo-container flex items-center"> |
|
<div class="logo mr-4"> |
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="48" height="48"> |
|
<image xlink:href="https://placehold.co/100x100/3B82F6/white?text=ر" width="100" height="100"/> |
|
</svg> |
|
</div> |
|
<div class="company-name text-2xl font-bold">شركة الريحان للترجمة</div> |
|
</div> |
|
</div> |
|
<h1 class="text-5xl font-bold text-center mb-4 animate-scale">النظام المراجع الذكي</h1> |
|
<p class="text-center text-xl text-blue-100 opacity-90">مقارنة وتحليل النصوص - المصدر مرجع أساسي</p> |
|
</div> |
|
</header> |
|
|
|
|
|
<section id="programExplanation" class="max-w-6xl mx-auto px-4 mb-8"> |
|
<div class="bg-white-classic rounded-2xl shadow-lg p-8 border border-gray-100"> |
|
<p class="text-lg text-gray-800"> |
|
هذا البرنامج مصمم لمقارنة النصوص التقنية وتحليلها بدقة عالية، مع تحديد الاختلافات في الأرقام، النصوص المفقودة واختلافات المعنى. يمكنك استخدام نظام OCR لاستخراج النصوص من ملفات PDF والصور، مع إمكانية تحرير الصور وقصها وتدويرها قبل الاستخراج. |
|
</p> |
|
</div> |
|
</section> |
|
|
|
|
|
<main class="max-w-6xl mx-auto px-4"> |
|
|
|
<div class="bg-white-classic rounded-2xl shadow-lg p-8 border border-gray-100 hover:shadow-xl transition-all animate-scale mb-8 card-hover"> |
|
<h2 class="text-2xl font-bold mb-6 text-gray-800 flex items-center border-b pb-3"> |
|
<i class="fas fa-file-upload text-blue-600 ml-2"></i> تحميل الملفات |
|
</h2> |
|
|
|
|
|
<div class="stats-container mb-4"> |
|
<div class="stats-badge"> |
|
عدد الصفحات المعالجة: <span id="ocrCounter">0</span> |
|
</div> |
|
<div class="stats-badge"> |
|
تاريخ آخر معالجة: <span id="lastOcrDate">-</span> |
|
</div> |
|
</div> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|
|
|
<div class="text-center"> |
|
<div class="group border-2 border-dashed border-blue-300 rounded-xl p-8 text-center hover:border-blue-500 transition-colors duration-300 bg-blue-50 hover:bg-blue-100"> |
|
<label class="cursor-pointer block"> |
|
<input type="file" id="sourceFile" accept=".docx,.pdf,.jpg,.jpeg,.png" class="hidden"> |
|
<i class="fas fa-file-upload text-5xl text-blue-500 mb-4"></i> |
|
<span class="text-lg text-blue-600 group-hover:text-blue-700">ملف السورس</span> |
|
</label> |
|
</div> |
|
<div class="mt-2"> |
|
<button id="processSourceBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> |
|
معالجة OCR للسورس |
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="text-center"> |
|
<div class="group border-2 border-dashed border-blue-300 rounded-xl p-8 text-center hover:border-blue-500 transition-colors duration-300 bg-blue-50 hover:bg-blue-100"> |
|
<label class="cursor-pointer block"> |
|
<input type="file" id="targetFile" accept=".docx,.pdf,.jpg,.jpeg,.png" class="hidden"> |
|
<i class="fas fa-file-download text-5xl text-blue-500 mb-4"></i> |
|
<span class="text-lg text-blue-600 group-hover:text-blue-700">ملف التارجت</span> |
|
</label> |
|
</div> |
|
<div class="mt-2"> |
|
<button id="processTargetBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> |
|
معالجة OCR للتارجت |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="processingStatus" class="hidden mt-4"> |
|
<div class="alert alert-info bg-blue-50 border border-blue-200 rounded-xl p-4 my-4"> |
|
<div class="flex items-center"> |
|
<div class="animate-spin h-6 w-6 border-4 border-blue-600 rounded-full border-t-transparent ml-3"></div> |
|
<span id="statusText">جاري معالجة الملف...</span> |
|
</div> |
|
</div> |
|
<div class="progress mt-2"> |
|
<div id="progressBar" class="progress-bar" style="width: 0%"></div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="pdfPagesCard" class="hidden mt-6"> |
|
<h3 class="text-xl font-bold mb-4 text-gray-800">صفحات الملف</h3> |
|
<p>اختر الصفحات التي تريد معالجتها (انقر للتحديد)</p> |
|
|
|
<div class="mb-3"> |
|
<button id="selectAllBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-1 px-3 rounded text-sm ml-2">تحديد الكل</button> |
|
<button id="deselectAllBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-1 px-3 rounded text-sm">إلغاء تحديد الكل</button> |
|
</div> |
|
|
|
<div id="pdfPagesContainer" class="pdf-grid"></div> |
|
|
|
<div class="mt-4 flex items-center space-x-2"> |
|
<div class="edit-toolbar flex flex-wrap gap-2"> |
|
<button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="rotateLeft"> |
|
<i class="fas fa-undo ml-1"></i> تدوير لليسار |
|
</button> |
|
<button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="rotateRight"> |
|
<i class="fas fa-redo ml-1"></i> تدوير لليمين |
|
</button> |
|
<button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="flipHorizontal"> |
|
<i class="fas fa-arrows-alt-h ml-1"></i> قلب أفقي |
|
</button> |
|
<button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="flipVertical"> |
|
<i class="fas fa-arrows-alt-v ml-1"></i> قلب عمودي |
|
</button> |
|
<button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="cropImage"> |
|
<i class="fas fa-crop ml-1"></i> قص الصورة |
|
</button> |
|
<button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="resetImage"> |
|
<i class="fas fa-sync-alt ml-1"></i> إعادة ضبط |
|
</button> |
|
<button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="improveContrast"> |
|
<i class="fas fa-adjust ml-1"></i> تحسين التباين |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-3"> |
|
<button id="extractTextBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> |
|
<i class="fas fa-magic ml-2"></i> استخراج النص |
|
</button> |
|
</div> |
|
|
|
|
|
<div id="imageEditor" class="hidden mt-4"> |
|
<div class="border border-gray-300 rounded-lg bg-gray-100 p-2"> |
|
<canvas id="imageCanvas" class="max-w-full"></canvas> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="resultsCard" class="hidden mt-6"> |
|
<h3 class="text-xl font-bold mb-4 text-gray-800">النص المستخرج</h3> |
|
<div id="resultPreview"></div> |
|
<div id="resultTextContainer" class="mt-4"> |
|
<div class="flex justify-between items-center mb-2"> |
|
<h4 class="font-bold text-gray-800">النص:</h4> |
|
<button id="copyTextBtn" class="bg-blue-100 hover:bg-blue-200 text-blue-800 px-3 py-1 rounded-lg text-sm"> |
|
<i class="far fa-copy ml-1"></i> نسخ النص |
|
</button> |
|
</div> |
|
<div id="resultText" class="result-text"> |
|
لم يتم استخراج نص بعد. |
|
</div> |
|
</div> |
|
<div class="mt-3 flex space-x-2"> |
|
<button id="useAsSourceBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> |
|
استخدام النص كنص مصدر |
|
</button> |
|
<button id="useAsTargetBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> |
|
استخدام النص كنص هدف |
|
</button> |
|
<button id="downloadTextBtn" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded transition-all"> |
|
تنزيل النص |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white-classic rounded-2xl shadow-lg p-8 border border-gray-100 hover:shadow-xl transition-all animate-scale mb-8 card-hover"> |
|
<div class="space-y-6"> |
|
|
|
<div class="group"> |
|
<label class="block text-lg font-bold text-gray-700 mb-3 flex items-center"> |
|
<i class="fas fa-language text-blue-600 ml-2"></i> النص المصدر |
|
</label> |
|
<textarea id="sourceText" dir="rtl" class="w-full px-6 py-4 border-2 border-gray-200 rounded-xl focus:ring-blue-200 focus:border-blue-400 transition-all resize-none text-lg" rows="6" placeholder="اكتب النص المصدر هنا..."></textarea> |
|
</div> |
|
|
|
<div class="group"> |
|
<label class="block text-lg font-bold text-gray-700 mb-3 flex items-center"> |
|
<i class="fas fa-language text-blue-600 ml-2"></i> النص الهدف |
|
</label> |
|
<textarea id="targetText" dir="ltr" class="w-full px-6 py-4 border-2 border-gray-200 rounded-xl focus:ring-blue-200 focus:border-blue-400 transition-all resize-none text-lg" rows="6" placeholder="اكتب النص الهدف هنا..."></textarea> |
|
</div> |
|
|
|
<div class="mt-4 flex items-center"> |
|
<input type="checkbox" id="terminologyCheck" class="mr-2"> |
|
<label for="terminologyCheck" class="text-lg text-gray-700">استخدام قاعدة المصطلحات</label> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white-classic rounded-2xl shadow-lg p-8 border border-gray-100 hover:shadow-xl transition-all animate-scale mb-8 card-hover"> |
|
<h2 class="text-2xl font-bold mb-6 text-gray-800 flex items-center border-b pb-3"> |
|
<i class="fas fa-book-open text-blue-600 ml-2"></i> مصادر إضافية |
|
</h2> |
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|
|
|
<div class="group border-2 border-dashed border-blue-300 rounded-xl p-8 text-center hover:border-blue-500 transition-colors duration-300 bg-blue-50 hover:bg-blue-100"> |
|
<label class="cursor-pointer block"> |
|
<input type="file" id="sourceExtraFile" accept=".docx,.pdf" class="hidden"> |
|
<i class="fas fa-upload text-5xl text-blue-500 mb-4"></i> |
|
<span class="text-lg text-blue-600 group-hover:text-blue-700">تحميل ملف المصدر</span> |
|
</label> |
|
</div> |
|
|
|
<div class="group"> |
|
<label class="block text-lg font-bold text-gray-700 mb-3 flex items-center"> |
|
<i class="fas fa-edit text-blue-600 ml-2"></i> إدخال المصادر يدويًا |
|
</label> |
|
<textarea id="sourceExtraText" dir="rtl" class="w-full px-6 py-4 border-2 border-blue-200 rounded-xl focus:ring-blue-200 focus:border-blue-400 transition-all resize-none text-lg" rows="6" placeholder="اكتب المصادر هنا..."></textarea> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<button id="submitBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-5 px-8 rounded-xl transition-all transform hover:scale-105 focus:ring-blue-200 text-xl shadow-lg hover:shadow-xl mb-8"> |
|
<div class="flex items-center justify-center"> |
|
<i class="fas fa-sync-alt ml-2"></i> تحليل النصوص |
|
</div> |
|
</button> |
|
|
|
|
|
<div id="resultSection" class="bg-white-classic rounded-2xl shadow-lg p-8 border border-gray-100 hover:shadow-xl transition-all animate-scale mb-8 hidden card-hover"> |
|
<h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-3 flex items-center"> |
|
<i class="fas fa-search text-blue-600 ml-2"></i> نتائج التحليل والمقارنة |
|
</h2> |
|
<div id="errorsList" class="space-y-3 mb-6"></div> |
|
|
|
|
|
<div class="view-tabs"> |
|
<div class="view-tab active" data-tab="classic-view">طريقة العرض الكلاسيكية</div> |
|
<div class="view-tab" data-tab="segment-view">طريقة العرض المقسمة</div> |
|
<div class="view-tab" data-tab="interactive-view">طريقة العرض التفاعلية</div> |
|
</div> |
|
|
|
|
|
<div id="classic-view" class="view-content active"> |
|
<div class="result-section split-view"> |
|
|
|
<div> |
|
<h4 class="text-lg font-bold text-gray-700 mb-3 flex items-center"> |
|
<i class="fas fa-file-alt text-blue-600 ml-2"></i> النص المصدر (مع التعليم) |
|
</h4> |
|
<div id="sourceTextReview" class="bg-blue-50 rounded-xl p-6 min-h-[200px] border-2 border-blue-100 text-comparison"></div> |
|
</div> |
|
|
|
<div> |
|
<h4 class="text-lg font-bold text-gray-700 mb-3 flex items-center"> |
|
<i class="fas fa-file-alt text-blue-600 ml-2"></i> النص الهدف (مع التعليم) |
|
</h4> |
|
<div id="targetTextReview" class="bg-gray-50 rounded-xl p-6 min-h-[200px] border-2 border-gray-200 text-comparison"></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="segment-view" class="view-content"> |
|
<div id="segmentedComparisonContainer" class="space-y-4"> |
|
|
|
</div> |
|
</div> |
|
|
|
|
|
<div id="interactive-view" class="view-content"> |
|
<div class="bg-blue-50 rounded-xl p-6 mb-4"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h4 class="text-lg font-bold text-blue-800">عرض تفاعلي للاختلافات</h4> |
|
<div class="flex space-x-2"> |
|
<button id="prevDiff" class="bg-blue-200 hover:bg-blue-300 text-blue-800 px-3 py-1 rounded-lg disabled:opacity-50"> |
|
<i class="fas fa-arrow-right ml-1"></i> السابق |
|
</button> |
|
<button id="nextDiff" class="bg-blue-200 hover:bg-blue-300 text-blue-800 px-3 py-1 rounded-lg disabled:opacity-50"> |
|
<i class="fas fa-arrow-left ml-1"></i> التالي |
|
</button> |
|
<span id="diffCounter" class="bg-white px-3 py-1 rounded-lg">0/0</span> |
|
</div> |
|
</div> |
|
<div id="currentDiffDisplay" class="bg-white rounded-lg p-4 min-h-[100px] border border-blue-200"> |
|
<p class="text-gray-500 text-center">اضغط "التالي" للبدء في استعراض الاختلافات</p> |
|
</div> |
|
</div> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
<div class="bg-blue-50 rounded-xl p-4 border border-blue-100"> |
|
<h5 class="font-bold text-blue-800 mb-2">ملخص الاختلافات</h5> |
|
<div id="diffSummary" class="space-y-2"> |
|
<div class="flex items-center justify-between bg-white p-2 rounded"> |
|
<span>اختلافات الأرقام:</span> |
|
<span id="numberDiffCount" class="bg-yellow-100 px-2 py-1 rounded font-bold">0</span> |
|
</div> |
|
<div class="flex items-center justify-between bg-white p-2 rounded"> |
|
<span>النصوص المفقودة:</span> |
|
<span id="missingTextCount" class="bg-blue-100 px-2 py-1 rounded font-bold">0</span> |
|
</div> |
|
<div class="flex items-center justify-between bg-white p-2 rounded"> |
|
<span>اختلافات المعنى:</span> |
|
<span id="meaningDiffCount" class="bg-red-100 px-2 py-1 rounded font-bold">0</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="bg-green-50 rounded-xl p-4 border border-green-100"> |
|
<h5 class="font-bold text-green-800 mb-2">توصيات المعالجة</h5> |
|
<div id="diffRecommendations" class="space-y-2 text-green-800"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="explanationBox" class="bg-white-classic rounded-2xl shadow-lg p-8 border border-gray-100 hover:shadow-xl transition-all animate-scale mb-8 hidden card-hover"> |
|
<h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-3 flex items-center"> |
|
<i class="fas fa-info-circle text-blue-600 ml-2"></i> شرح الاختلافات |
|
</h2> |
|
<div id="explanationText" class="text-lg text-gray-700"></div> |
|
</div> |
|
|
|
|
|
<div id="fullTextDraftSection" class="bg-white-classic rounded-2xl shadow-lg p-8 border border-gray-100 transition-all animate-scale mb-8 hidden card-hover"> |
|
<h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-3 flex items-center"> |
|
<i class="fas fa-file-alt text-blue-600 ml-2"></i> مسودة التحليل النصي الكامل |
|
<span class="draft-marker mr-2">مسودة</span> |
|
</h2> |
|
<div id="paragraphDivisionsContainer" class="mt-4 space-y-2"> |
|
|
|
</div> |
|
</div> |
|
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8 space-y-4 md:space-y-0"> |
|
<button id="toggleDraftBtn" class="w-full md:w-auto bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> |
|
عرض/إخفاء مسودة التحليل |
|
</button> |
|
|
|
<div class="flex space-x-4"> |
|
<button id="downloadExcelBtn" class="w-full md:w-auto bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"> |
|
تنزيل التقرير (Excel) |
|
</button> |
|
<button id="downloadWordBtn" class="w-full md:w-auto bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> |
|
تنزيل التقرير (Word) |
|
</button> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
|
|
|
|
|
|
<script> |
|
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js'; |
|
|
|
|
|
const DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions'; |
|
const DEEPSEEK_API_KEY = 'sk-15606736ed9e4aea8b7cc11a195d2b01'; |
|
|
|
|
|
const CHATGPT_API_URL = 'https://api.openai.com/v1/chat/completions'; |
|
const CHATGPT_API_KEY = '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'; |
|
|
|
|
|
const RAPIDAPI_KEY = 'eb11693cddmshb8bd157e05b74acp1f6aa4jsn4369fa546e55'; |
|
const OCR_API_URL = 'https://ocr43.p.rapidapi.com/v1/results'; |
|
|
|
|
|
let fullAnalysisText = ''; |
|
let analysisSegments = []; |
|
let allDifferences = []; |
|
let currentDiffIndex = -1; |
|
|
|
|
|
let ocrPagesCount = 0; |
|
let documentPages = []; |
|
let selectedPages = []; |
|
let extractedTexts = []; |
|
let extractedPageNumbers = []; |
|
let currentProcessingMode = ''; |
|
|
|
|
|
let canvas = null; |
|
let fabricCanvas = null; |
|
let originalImageData = null; |
|
let isInCropMode = false; |
|
let cropRect = null; |
|
let currentPageIndex = 0; |
|
|
|
|
|
|
|
|
|
|
|
const ANALYSIS_PROMPT = `أنت خبير لغوي متخصص في مراجعة الترجمة التقنية. مهمتك مقارنة النص المصدر والنص الهدف بدقة عالية مع العلم أن النصوص مليانة بالأخطاء والنواقص. |
|
|
|
لا تقم بإزالة أو تعديل العلامات التالية: |
|
• الأرقام: تحافظ على علامات < و >. |
|
• النصوص المفقودة: تحافظ على علامات __ و __. |
|
• اختلافات المعنى: تحافظ على علامات [MEANING] و [/MEANING]. |
|
|
|
معالجة الأرقام: |
|
1. اعتبر الأرقام بمختلف أنظمتها (العربية، الهندية، الإنجليزية) متطابقة إذا كانت تمثل نفس القيمة. |
|
2. مثال: ٣ و 3 و ۳ كلها تمثل نفس الرقم، لا تعتبرها خطأ. |
|
3. حدد فقط الأرقام المختلفة في القيمة الفعلية. |
|
|
|
اعتمد النص المصدر كأساس للمقارنة، وقم بتحديد: |
|
1. اختلافات الأرقام باستخدام <الرقم_في_المصدر> → <الرقم_في_الهدف>. |
|
2. النصوص المفقودة كما هي بين علامتي __. |
|
3. اختلافات المعنى باستخدام [MEANING] مع الحفاظ على التعليم. |
|
|
|
احرص على استخراج الجداول والتنسيقات الخاصة بشكل دقيق. |
|
|
|
النص المصدر: |
|
{source} |
|
|
|
النص الهدف: |
|
{target}`; |
|
|
|
|
|
const OCR_PROMPT = `قم بمعالجة هذه الصورة بدقة عالية واستخراج كل النص الموجود فيها. |
|
|
|
احرص على: |
|
1. استخراج جميع النصوص بدقة عالية، حتى الصغيرة منها أو الباهتة. |
|
2. الحفاظ على تنسيق الفقرات والتباعد بين الأسطر. |
|
3. التمييز بين الجداول والنصوص العادية. |
|
4. استخراج محتوى الجداول بشكل منظم مع الحفاظ على صفوفها وأعمدتها. |
|
5. تحديد النصوص غير المؤكدة بين علامتي [?...?]. |
|
|
|
قم بتقديم النتائج على النحو التالي: |
|
1. أولاً: النص الكامل المستخرج بتنسيقه الأصلي. |
|
2. ثانياً: إذا وجدت جداول، قدم كل جدول بشكل منفصل مع وصف بنيته. |
|
|
|
الصورة للمعالجة: |
|
{image_data}`; |
|
|
|
|
|
const TEXT_DIVISION_PROMPT = `قم بتقسيم النص التالي إلى أجزاء منطقية للتحليل. |
|
يجب أن يكون كل جزء وحدة متماسكة (فقرة، قسم، إلخ) لا تتجاوز 500 كلمة. |
|
أعد النص كمصفوفة JSON بالأجزاء. الصيغة: {"segments": ["جزء1", "جزء2", ...]} |
|
|
|
النص للتقسيم: |
|
{text}`; |
|
|
|
|
|
const SYNC_WORD_PROMPT = `قم بتحليل النص التالي وإيجاد آخر كلمة أو جملة قصيرة (لا تزيد عن 3 كلمات) مناسبة ليمكن استخدامها كنقطة تزامن بين الفقرات. اختر كلمة أو جملة تظهر بشكل طبيعي في النص وتكون مناسبة للتقسيم. |
|
أعِد فقط الكلمة أو الجملة المحددة دون أي إضافات أخرى. |
|
|
|
النص: |
|
{text}`; |
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const savedCount = localStorage.getItem('ocrPagesCount'); |
|
const lastDate = localStorage.getItem('lastOcrDate'); |
|
|
|
if (savedCount) { |
|
ocrPagesCount = parseInt(savedCount); |
|
document.getElementById('ocrCounter').textContent = ocrPagesCount; |
|
} |
|
|
|
if (lastDate) { |
|
document.getElementById('lastOcrDate').textContent = lastDate; |
|
} |
|
|
|
|
|
document.getElementById('processSourceBtn').addEventListener('click', function() { |
|
processFileForOCR('source'); |
|
}); |
|
|
|
document.getElementById('processTargetBtn').addEventListener('click', function() { |
|
processFileForOCR('target'); |
|
}); |
|
|
|
|
|
document.getElementById('selectAllBtn')?.addEventListener('click', selectAllPages); |
|
document.getElementById('deselectAllBtn')?.addEventListener('click', deselectAllPages); |
|
document.getElementById('extractTextBtn')?.addEventListener('click', extractText); |
|
|
|
|
|
document.getElementById('copyTextBtn')?.addEventListener('click', copyText); |
|
document.getElementById('downloadTextBtn')?.addEventListener('click', downloadText); |
|
document.getElementById('useAsSourceBtn')?.addEventListener('click', function() { |
|
useOcrText('source'); |
|
}); |
|
document.getElementById('useAsTargetBtn')?.addEventListener('click', function() { |
|
useOcrText('target'); |
|
}); |
|
|
|
|
|
document.getElementById('rotateLeft')?.addEventListener('click', rotateImageLeft); |
|
document.getElementById('rotateRight')?.addEventListener('click', rotateImageRight); |
|
document.getElementById('flipHorizontal')?.addEventListener('click', flipImageHorizontal); |
|
document.getElementById('flipVertical')?.addEventListener('click', flipImageVertical); |
|
document.getElementById('cropImage')?.addEventListener('click', function() { |
|
if (isInCropMode) { |
|
applyCrop(); |
|
} else { |
|
activateCropMode(); |
|
} |
|
}); |
|
document.getElementById('resetImage')?.addEventListener('click', resetImage); |
|
document.getElementById('improveContrast')?.addEventListener('click', improveContrast); |
|
}); |
|
|
|
|
|
|
|
|
|
function normalizeNumbers(text) { |
|
|
|
return text.replace(/[٠١٢٣٤٥٦٧٨٩]/g, d => d.charCodeAt(0) - 1632) |
|
.replace(/[۰۱۲۳۴۵۶۷۸۹]/g, d => d.charCodeAt(0) - 1776); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateOcrCounter(count) { |
|
ocrPagesCount += count; |
|
document.getElementById('ocrCounter').textContent = ocrPagesCount; |
|
|
|
|
|
localStorage.setItem('ocrPagesCount', ocrPagesCount); |
|
|
|
|
|
const now = new Date(); |
|
const formattedDate = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; |
|
|
|
document.getElementById('lastOcrDate').textContent = formattedDate; |
|
localStorage.setItem('lastOcrDate', formattedDate); |
|
} |
|
|
|
|
|
async function processFileForOCR(mode) { |
|
currentProcessingMode = mode; |
|
const fileInput = mode === 'source' ? document.getElementById('sourceFile') : document.getElementById('targetFile'); |
|
|
|
if (!fileInput.files || fileInput.files.length === 0) { |
|
alert('الرجاء اختيار ملف أولاً'); |
|
return; |
|
} |
|
|
|
const file = fileInput.files[0]; |
|
|
|
|
|
document.getElementById('processingStatus').classList.remove('hidden'); |
|
document.getElementById('statusText').textContent = 'جاري معالجة الملف...'; |
|
document.getElementById('progressBar').style.width = '0%'; |
|
|
|
|
|
document.getElementById('pdfPagesCard').classList.add('hidden'); |
|
document.getElementById('resultsCard').classList.add('hidden'); |
|
|
|
|
|
documentPages = []; |
|
selectedPages = []; |
|
extractedTexts = []; |
|
extractedPageNumbers = []; |
|
|
|
try { |
|
const fileType = file.name.split('.').pop().toLowerCase(); |
|
|
|
if (fileType === 'pdf') { |
|
await processPdf(file); |
|
} else if (['jpg', 'jpeg', 'png'].includes(fileType)) { |
|
await processImage(file); |
|
} else { |
|
throw new Error('نوع الملف غير مدعوم. يرجى اختيار ملف PDF أو صورة.'); |
|
} |
|
|
|
|
|
document.getElementById('pdfPagesCard').classList.remove('hidden'); |
|
|
|
|
|
document.getElementById('processingStatus').classList.add('hidden'); |
|
|
|
} catch (error) { |
|
console.error('Error processing file:', error); |
|
document.getElementById('statusText').textContent = `خطأ: ${error.message}`; |
|
} |
|
} |
|
|
|
|
|
async function processPdf(file) { |
|
try { |
|
|
|
const arrayBuffer = await file.arrayBuffer(); |
|
const pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; |
|
|
|
|
|
const numPages = pdfDoc.numPages; |
|
if (numPages > 100) { |
|
alert('هذا الملف يحتوي على أكثر من 100 صفحة. سيتم معالجة أول 100 صفحة فقط.'); |
|
} |
|
|
|
|
|
const pdfPagesContainer = document.getElementById('pdfPagesContainer'); |
|
pdfPagesContainer.innerHTML = ''; |
|
|
|
|
|
const maxPages = Math.min(numPages, 100); |
|
for (let i = 1; i <= maxPages; i++) { |
|
|
|
document.getElementById('progressBar').style.width = `${(i / maxPages) * 100}%`; |
|
document.getElementById('statusText').textContent = `جاري معالجة الصفحة ${i} من ${maxPages}...`; |
|
|
|
|
|
const pageImage = await convertPdfPageToImage(pdfDoc, i); |
|
|
|
|
|
documentPages.push({ |
|
pageNumber: i, |
|
imageData: pageImage.imageData, |
|
width: pageImage.width, |
|
height: pageImage.height |
|
}); |
|
|
|
|
|
createPageElement(pageImage.imageData, i); |
|
} |
|
|
|
} catch (error) { |
|
console.error('Error processing PDF:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
|
|
async function processImage(file) { |
|
try { |
|
|
|
const imageData = await readFileAsDataURL(file); |
|
|
|
|
|
const img = new Image(); |
|
await new Promise((resolve, reject) => { |
|
img.onload = resolve; |
|
img.onerror = reject; |
|
img.src = imageData; |
|
}); |
|
|
|
|
|
const pdfPagesContainer = document.getElementById('pdfPagesContainer'); |
|
pdfPagesContainer.innerHTML = ''; |
|
|
|
|
|
documentPages = [{ |
|
pageNumber: 1, |
|
imageData: imageData, |
|
width: img.width, |
|
height: img.height |
|
}]; |
|
|
|
|
|
createPageElement(imageData, 1); |
|
|
|
|
|
document.getElementById('progressBar').style.width = '100%'; |
|
document.getElementById('statusText').textContent = 'تمت معالجة الصورة بنجاح'; |
|
|
|
} catch (error) { |
|
console.error('Error processing image:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
|
|
async function convertPdfPageToImage(pdfDoc, pageNumber, scale = 1.5) { |
|
try { |
|
|
|
const page = await pdfDoc.getPage(pageNumber); |
|
|
|
|
|
const canvas = document.createElement('canvas'); |
|
const context = canvas.getContext('2d'); |
|
|
|
|
|
const viewport = page.getViewport({ scale }); |
|
canvas.width = viewport.width; |
|
canvas.height = viewport.height; |
|
|
|
|
|
await page.render({ |
|
canvasContext: context, |
|
viewport: viewport |
|
}).promise; |
|
|
|
|
|
return { |
|
imageData: canvas.toDataURL('image/png'), |
|
width: viewport.width, |
|
height: viewport.height, |
|
pageNumber: pageNumber |
|
}; |
|
} catch (error) { |
|
console.error(`Error converting PDF page ${pageNumber} to image:`, error); |
|
throw error; |
|
} |
|
} |
|
|
|
|
|
function createPageElement(imageData, pageNumber) { |
|
const pageDiv = document.createElement('div'); |
|
pageDiv.className = 'pdf-page'; |
|
pageDiv.dataset.page = pageNumber; |
|
|
|
const img = document.createElement('img'); |
|
img.src = imageData; |
|
img.alt = `صفحة ${pageNumber}`; |
|
|
|
const pageNumberDiv = document.createElement('div'); |
|
pageNumberDiv.className = 'page-number'; |
|
pageNumberDiv.textContent = `صفحة ${pageNumber}`; |
|
|
|
pageDiv.appendChild(img); |
|
pageDiv.appendChild(pageNumberDiv); |
|
|
|
|
|
pageDiv.addEventListener('click', function() { |
|
|
|
this.classList.toggle('selected'); |
|
|
|
|
|
const page = parseInt(this.dataset.page); |
|
if (this.classList.contains('selected')) { |
|
if (!selectedPages.includes(page)) { |
|
selectedPages.push(page); |
|
} |
|
} else { |
|
const index = selectedPages.indexOf(page); |
|
if (index > -1) { |
|
selectedPages.splice(index, 1); |
|
} |
|
} |
|
|
|
|
|
if (this.classList.contains('selected') && documentPages.length > 0) { |
|
const selectedPage = documentPages.find(p => p.pageNumber === page); |
|
if (selectedPage) { |
|
displayImageForEditing(selectedPage.imageData, page); |
|
} |
|
} |
|
}); |
|
|
|
document.getElementById('pdfPagesContainer').appendChild(pageDiv); |
|
} |
|
|
|
|
|
function displayImageForEditing(imageData, pageNumber) { |
|
currentPageIndex = pageNumber; |
|
|
|
|
|
document.getElementById('imageEditor').classList.remove('hidden'); |
|
|
|
|
|
const img = new Image(); |
|
img.onload = function() { |
|
|
|
canvas = document.getElementById('imageCanvas'); |
|
canvas.width = img.width; |
|
canvas.height = img.height; |
|
const context = canvas.getContext('2d'); |
|
|
|
|
|
context.drawImage(img, 0, 0, img.width, img.height); |
|
|
|
|
|
originalImageData = context.getImageData(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
initFabricCanvas(); |
|
}; |
|
img.src = imageData; |
|
} |
|
|
|
|
|
function initFabricCanvas() { |
|
|
|
if (fabricCanvas) { |
|
fabricCanvas.dispose(); |
|
} |
|
|
|
|
|
fabricCanvas = new fabric.Canvas('imageCanvas', { |
|
selection: false |
|
}); |
|
|
|
|
|
fabric.Image.fromURL(canvas.toDataURL(), function(img) { |
|
img.selectable = false; |
|
fabricCanvas.add(img); |
|
fabricCanvas.renderAll(); |
|
}); |
|
|
|
|
|
isInCropMode = false; |
|
if (cropRect) { |
|
fabricCanvas.remove(cropRect); |
|
cropRect = null; |
|
} |
|
} |
|
|
|
|
|
function rotateImageLeft() { |
|
if (!fabricCanvas) return; |
|
|
|
const objects = fabricCanvas.getObjects(); |
|
if (objects.length === 0) return; |
|
|
|
const img = objects[0]; |
|
img.rotate((img.angle || 0) - 90); |
|
fabricCanvas.renderAll(); |
|
} |
|
|
|
|
|
function rotateImageRight() { |
|
if (!fabricCanvas) return; |
|
|
|
const objects = fabricCanvas.getObjects(); |
|
if (objects.length === 0) return; |
|
|
|
const img = objects[0]; |
|
img.rotate((img.angle || 0) + 90); |
|
fabricCanvas.renderAll(); |
|
} |
|
|
|
|
|
function flipImageHorizontal() { |
|
if (!fabricCanvas) return; |
|
|
|
const objects = fabricCanvas.getObjects(); |
|
if (objects.length === 0) return; |
|
|
|
const img = objects[0]; |
|
img.set('flipX', !img.flipX); |
|
fabricCanvas.renderAll(); |
|
} |
|
|
|
|
|
function flipImageVertical() { |
|
if (!fabricCanvas) return; |
|
|
|
const objects = fabricCanvas.getObjects(); |
|
if (objects.length === 0) return; |
|
|
|
const img = objects[0]; |
|
img.set('flipY', !img.flipY); |
|
fabricCanvas.renderAll(); |
|
} |
|
|
|
|
|
function activateCropMode() { |
|
if (!fabricCanvas) return; |
|
|
|
|
|
if (isInCropMode) { |
|
if (cropRect) { |
|
fabricCanvas.remove(cropRect); |
|
cropRect = null; |
|
} |
|
isInCropMode = false; |
|
return; |
|
} |
|
|
|
|
|
const canvasWidth = fabricCanvas.getWidth(); |
|
const canvasHeight = fabricCanvas.getHeight(); |
|
|
|
cropRect = new fabric.Rect({ |
|
left: canvasWidth * 0.2, |
|
top: canvasHeight * 0.2, |
|
width: canvasWidth * 0.6, |
|
height: canvasHeight * 0.6, |
|
fill: 'rgba(0,0,0,0.1)', |
|
stroke: 'rgba(59, 130, 246, 0.9)', |
|
strokeWidth: 2, |
|
strokeDashArray: [5, 5], |
|
selectable: true, |
|
hasRotatingPoint: false |
|
}); |
|
|
|
fabricCanvas.add(cropRect); |
|
fabricCanvas.setActiveObject(cropRect); |
|
|
|
isInCropMode = true; |
|
} |
|
|
|
|
|
function applyCrop() { |
|
if (!fabricCanvas || !cropRect) return; |
|
|
|
const objects = fabricCanvas.getObjects(); |
|
if (objects.length < 2) return; |
|
|
|
const img = objects[0]; |
|
|
|
|
|
const imgWidth = img.getScaledWidth(); |
|
const imgHeight = img.getScaledHeight(); |
|
|
|
const cropX = cropRect.left - img.left; |
|
const cropY = cropRect.top - img.top; |
|
const cropWidth = cropRect.getScaledWidth(); |
|
const cropHeight = cropRect.getScaledHeight(); |
|
|
|
|
|
if (cropX < 0 || cropY < 0 || cropX + cropWidth > imgWidth || cropY + cropHeight > imgHeight) { |
|
alert('منطقة القص خارج حدود الصورة'); |
|
return; |
|
} |
|
|
|
|
|
const tempCanvas = document.createElement('canvas'); |
|
tempCanvas.width = cropWidth; |
|
tempCanvas.height = cropHeight; |
|
const tempCtx = tempCanvas.getContext('2d'); |
|
|
|
|
|
const originalCanvas = document.getElementById('imageCanvas'); |
|
tempCtx.drawImage( |
|
originalCanvas, |
|
cropX, cropY, cropWidth, cropHeight, |
|
0, 0, cropWidth, cropHeight |
|
); |
|
|
|
|
|
fabricCanvas.clear(); |
|
fabric.Image.fromURL(tempCanvas.toDataURL(), function(croppedImg) { |
|
croppedImg.selectable = false; |
|
fabricCanvas.add(croppedImg); |
|
fabricCanvas.renderAll(); |
|
}); |
|
|
|
|
|
isInCropMode = false; |
|
cropRect = null; |
|
} |
|
|
|
|
|
function resetImage() { |
|
if (!fabricCanvas || !originalImageData) return; |
|
|
|
|
|
const originalCanvas = document.getElementById('imageCanvas'); |
|
const ctx = originalCanvas.getContext('2d'); |
|
ctx.putImageData(originalImageData, 0, 0); |
|
|
|
|
|
initFabricCanvas(); |
|
} |
|
|
|
|
|
function improveContrast() { |
|
if (!fabricCanvas) return; |
|
|
|
const originalCanvas = document.getElementById('imageCanvas'); |
|
const ctx = originalCanvas.getContext('2d'); |
|
const imageData = ctx.getImageData(0, 0, originalCanvas.width, originalCanvas.height); |
|
const data = imageData.data; |
|
|
|
|
|
let min = 255; |
|
let max = 0; |
|
|
|
for (let i = 0; i < data.length; i += 4) { |
|
const r = data[i]; |
|
const g = data[i + 1]; |
|
const b = data[i + 2]; |
|
|
|
const value = (r + g + b) / 3; |
|
|
|
if (value < min) min = value; |
|
if (value > max) max = value; |
|
} |
|
|
|
|
|
const contrast = 1.5; |
|
const brightness = 20; |
|
|
|
for (let i = 0; i < data.length; i += 4) { |
|
data[i] = Math.max(0, Math.min(255, ((data[i] - 128) * contrast) + 128 + brightness)); |
|
data[i + 1] = Math.max(0, Math.min(255, ((data[i + 1] - 128) * contrast) + 128 + brightness)); |
|
data[i + 2] = Math.max(0, Math.min(255, ((data[i + 2] - 128) * contrast) + 128 + brightness)); |
|
} |
|
|
|
|
|
ctx.putImageData(imageData, 0, 0); |
|
|
|
|
|
initFabricCanvas(); |
|
} |
|
|
|
|
|
function selectAllPages() { |
|
document.querySelectorAll('.pdf-page').forEach(page => { |
|
page.classList.add('selected'); |
|
const pageNumber = parseInt(page.dataset.page); |
|
if (!selectedPages.includes(pageNumber)) { |
|
selectedPages.push(pageNumber); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function deselectAllPages() { |
|
document.querySelectorAll('.pdf-page').forEach(page => { |
|
page.classList.remove('selected'); |
|
}); |
|
selectedPages = []; |
|
} |
|
|
|
|
|
async function extractText() { |
|
if (documentPages.length === 0) { |
|
alert('لا توجد صفحات للمعالجة'); |
|
return; |
|
} |
|
|
|
|
|
const pagesToProcess = selectedPages.length > 0 |
|
? documentPages.filter(page => selectedPages.includes(page.pageNumber)) |
|
: documentPages; |
|
|
|
if (pagesToProcess.length === 0) { |
|
alert('الرجاء تحديد صفحة واحدة على الأقل'); |
|
return; |
|
} |
|
|
|
|
|
document.getElementById('processingStatus').classList.remove('hidden'); |
|
document.getElementById('statusText').textContent = 'جاري استخراج النص...'; |
|
document.getElementById('progressBar').style.width = '0%'; |
|
|
|
try { |
|
extractedTexts = []; |
|
extractedPageNumbers = []; |
|
|
|
|
|
for (let i = 0; i < pagesToProcess.length; i++) { |
|
const page = pagesToProcess[i]; |
|
|
|
|
|
document.getElementById('progressBar').style.width = `${((i + 1) / pagesToProcess.length) * 100}%`; |
|
document.getElementById('statusText').textContent = `جاري معالجة الصفحة ${i + 1} من ${pagesToProcess.length}...`; |
|
|
|
|
|
let imageData = page.imageData; |
|
if (fabricCanvas && currentPageIndex === page.pageNumber) { |
|
imageData = document.getElementById('imageCanvas').toDataURL('image/jpeg'); |
|
} |
|
|
|
|
|
const pageText = await extractTextFromImage(imageData, page.pageNumber); |
|
extractedTexts.push(pageText); |
|
extractedPageNumbers.push(page.pageNumber); |
|
} |
|
|
|
|
|
updateOcrCounter(pagesToProcess.length); |
|
|
|
|
|
const combinedText = extractedTexts.join('\n\n'); |
|
|
|
|
|
document.getElementById('resultText').textContent = combinedText; |
|
|
|
|
|
generateResultPreview(); |
|
|
|
document.getElementById('resultsCard').classList.remove('hidden'); |
|
|
|
|
|
document.getElementById('processingStatus').classList.add('hidden'); |
|
|
|
} catch (error) { |
|
console.error('Error extracting text:', error); |
|
document.getElementById('statusText').textContent = `خطأ: ${error.message}`; |
|
} |
|
} |
|
|
|
|
|
function generateResultPreview() { |
|
const resultPreview = document.getElementById('resultPreview'); |
|
resultPreview.innerHTML = ''; |
|
|
|
if (extractedTexts.length === 0) { |
|
return; |
|
} |
|
|
|
|
|
const previewPages = Math.min(extractedTexts.length, 3); |
|
|
|
for (let i = 0; i < previewPages; i++) { |
|
const pageText = extractedTexts[i]; |
|
const pageNumber = extractedPageNumbers[i]; |
|
|
|
|
|
const previewDiv = document.createElement('div'); |
|
previewDiv.className = 'page-preview'; |
|
|
|
const pageTitle = document.createElement('h4'); |
|
pageTitle.textContent = `صفحة ${pageNumber}`; |
|
|
|
const textPreview = document.createElement('p'); |
|
|
|
const textContent = pageText.replace(`=== صفحة ${pageNumber} ===\n`, ''); |
|
textPreview.textContent = textContent.length > 100 ? |
|
textContent.substring(0, 100) + '...' : |
|
textContent; |
|
|
|
previewDiv.appendChild(pageTitle); |
|
previewDiv.appendChild(textPreview); |
|
resultPreview.appendChild(previewDiv); |
|
} |
|
|
|
|
|
if (extractedTexts.length > 3) { |
|
const morePages = document.createElement('p'); |
|
morePages.className = 'text-gray-500 text-center'; |
|
morePages.textContent = `+ ${extractedTexts.length - 3} صفحات أخرى...`; |
|
resultPreview.appendChild(morePages); |
|
} |
|
} |
|
|
|
|
|
async function extractTextFromImage(imageData, pageNumber) { |
|
try { |
|
|
|
const response = await fetch(imageData); |
|
const blob = await response.blob(); |
|
|
|
|
|
const formData = new FormData(); |
|
formData.append('image', blob, `page_${pageNumber}.png`); |
|
|
|
|
|
const ocrResponse = await fetch(OCR_API_URL, { |
|
method: 'POST', |
|
headers: { |
|
'X-RapidAPI-Key': RAPIDAPI_KEY, |
|
'X-RapidAPI-Host': 'ocr43.p.rapidapi.com' |
|
}, |
|
body: formData |
|
}); |
|
|
|
if (!ocrResponse.ok) { |
|
throw new Error(`فشل في طلب OCR: ${ocrResponse.status}`); |
|
} |
|
|
|
const data = await ocrResponse.json(); |
|
|
|
try { |
|
|
|
const text = data.results[0].entities[0].objects[0].entities[0].text; |
|
return `=== صفحة ${pageNumber} ===\n${text}`; |
|
} catch (e) { |
|
console.error('Error parsing OCR response:', e); |
|
return `=== صفحة ${pageNumber} ===\n[خطأ في معالجة النص]`; |
|
} |
|
} catch (error) { |
|
console.error(`Error in OCR for page ${pageNumber}:`, error); |
|
throw error; |
|
} |
|
} |
|
|
|
|
|
function copyText() { |
|
const resultText = document.getElementById('resultText'); |
|
const text = resultText.textContent; |
|
if (!text || text === 'لم يتم استخراج نص بعد.') { |
|
alert('لا يوجد نص للنسخ'); |
|
return; |
|
} |
|
|
|
navigator.clipboard.writeText(text) |
|
.then(() => { |
|
const copyBtn = document.getElementById('copyTextBtn'); |
|
copyBtn.innerHTML = '<i class="fas fa-check ml-1"></i> تم النسخ'; |
|
setTimeout(() => { |
|
copyBtn.innerHTML = '<i class="far fa-copy ml-1"></i> نسخ النص'; |
|
}, 2000); |
|
}) |
|
.catch(err => alert('حدث خطأ أثناء نسخ النص: ' + err)); |
|
} |
|
|
|
|
|
function downloadText() { |
|
const text = document.getElementById('resultText').textContent; |
|
if (!text || text === 'لم يتم استخراج نص بعد.') { |
|
alert('لا يوجد نص للتنزيل'); |
|
return; |
|
} |
|
|
|
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); |
|
const url = URL.createObjectURL(blob); |
|
const a = document.createElement('a'); |
|
a.href = url; |
|
a.download = 'alrihan_ocr_text.txt'; |
|
document.body.appendChild(a); |
|
a.click(); |
|
document.body.removeChild(a); |
|
URL.revokeObjectURL(url); |
|
} |
|
|
|
|
|
function useOcrText(targetField) { |
|
const resultText = document.getElementById('resultText'); |
|
const text = resultText.textContent; |
|
if (!text || text === 'لم يتم استخراج نص بعد.') { |
|
alert('لا يوجد نص للاستخدام'); |
|
return; |
|
} |
|
|
|
|
|
const cleanText = text.replace(/=== صفحة \d+ ===\n/g, ''); |
|
|
|
if (targetField === 'source') { |
|
document.getElementById('sourceText').value = cleanText; |
|
addError('تم نقل النص المستخرج إلى حقل النص المصدر', 'info'); |
|
} else { |
|
document.getElementById('targetText').value = cleanText; |
|
addError('تم نقل النص المستخرج إلى حقل النص الهدف', 'info'); |
|
} |
|
} |
|
|
|
|
|
function readFileAsDataURL(file) { |
|
return new Promise((resolve, reject) => { |
|
const reader = new FileReader(); |
|
reader.onload = e => resolve(e.target.result); |
|
reader.onerror = reject; |
|
reader.readAsDataURL(file); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
async function divideTextWithChatGPT(text) { |
|
try { |
|
|
|
if (countWords(text) <= 500) { |
|
return [text]; |
|
} |
|
|
|
|
|
const prompt = TEXT_DIVISION_PROMPT.replace("{text}", text); |
|
const payload = { |
|
model: "gpt-4o", |
|
messages: [ |
|
{ role: "system", content: "أنت مساعد متخصص في تقسيم النصوص إلى أجزاء منطقية." }, |
|
{ role: "user", content: prompt } |
|
], |
|
temperature: 0.3, |
|
max_tokens: 1024 |
|
}; |
|
|
|
|
|
const response = await fetch(CHATGPT_API_URL, { |
|
method: 'POST', |
|
headers: { |
|
'Authorization': 'Bearer ' + CHATGPT_API_KEY, |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify(payload) |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error('خطأ في API ChatGPT: ' + response.statusText); |
|
} |
|
|
|
const data = await response.json(); |
|
const content = data.choices[0].message.content.trim(); |
|
|
|
|
|
try { |
|
const jsonResponse = JSON.parse(content); |
|
if (jsonResponse.segments && Array.isArray(jsonResponse.segments)) { |
|
return jsonResponse.segments; |
|
} |
|
} catch (e) { |
|
|
|
const segments = content.split(/\n{2,}/).filter(seg => seg.trim().length > 0); |
|
if (segments.length > 0) { |
|
return segments; |
|
} |
|
} |
|
|
|
|
|
return text.split(/\n{2,}/).filter(para => para.trim().length > 0); |
|
|
|
} catch (error) { |
|
console.error('خطأ في تقسيم النص باستخدام ChatGPT:', error); |
|
|
|
return text.split(/\n{2,}/).filter(para => para.trim().length > 0); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
async function findSyncWord(text) { |
|
try { |
|
|
|
const prompt = SYNC_WORD_PROMPT.replace("{text}", text); |
|
const payload = { |
|
model: "gpt-4o", |
|
messages: [ |
|
{ role: "system", content: "أنت مساعد متخصص في تحليل النصوص وإيجاد نقاط تزامن مناسبة." }, |
|
{ role: "user", content: prompt } |
|
], |
|
temperature: 0.3, |
|
max_tokens: 50 |
|
}; |
|
|
|
|
|
const response = await fetch(CHATGPT_API_URL, { |
|
method: 'POST', |
|
headers: { |
|
'Authorization': 'Bearer ' + CHATGPT_API_KEY, |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify(payload) |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error('خطأ في API ChatGPT: ' + response.statusText); |
|
} |
|
|
|
const data = await response.json(); |
|
const syncWord = data.choices[0].message.content.trim(); |
|
|
|
return syncWord; |
|
} catch (error) { |
|
console.error('خطأ في إيجاد الكلمة المتزامنة:', error); |
|
|
|
return "نهاية الفقرة"; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
async function analyzeTextSegments(sourceSegments, targetSegments) { |
|
|
|
if (sourceSegments.length !== targetSegments.length) { |
|
throw new Error('عدد أجزاء المصدر والهدف غير متطابق'); |
|
} |
|
|
|
let combinedAnalysis = ''; |
|
const progressDiv = document.createElement('div'); |
|
progressDiv.className = "bg-blue-100 p-4 rounded-xl mb-4"; |
|
progressDiv.innerHTML = `<div class="flex items-center justify-between"> |
|
<div class="flex items-center"> |
|
<div class="animate-spin h-6 w-6 border-4 border-blue-600 rounded-full border-t-transparent ml-3"></div> |
|
<span>جارٍ التحليل...</span> |
|
</div> |
|
<span id="segmentProgress">0/${sourceSegments.length}</span> |
|
</div>`; |
|
document.getElementById('errorsList').appendChild(progressDiv); |
|
|
|
|
|
analysisSegments = []; |
|
|
|
|
|
for (let i = 0; i < sourceSegments.length; i++) { |
|
|
|
document.getElementById('segmentProgress').textContent = `${i+1}/${sourceSegments.length}`; |
|
|
|
|
|
const prompt = ANALYSIS_PROMPT |
|
.replace("{source}", sourceSegments[i]) |
|
.replace("{target}", targetSegments[i]); |
|
|
|
|
|
const payload = { |
|
model: "deepseek-reasoner", |
|
messages: [ |
|
{ role: "system", content: "أنت خبير في تحليل النصوص ومقارنتها بدقة عالية." }, |
|
{ role: "user", content: prompt } |
|
], |
|
temperature: 0.3, |
|
max_tokens: 2048, |
|
stream: false |
|
}; |
|
|
|
const response = await fetch(DEEPSEEK_API_URL, { |
|
method: 'POST', |
|
headers: { |
|
'Authorization': 'Bearer ' + DEEPSEEK_API_KEY, |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify(payload) |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error('حدث خطأ بالشبكة: ' + response.statusText); |
|
} |
|
|
|
const data = await response.json(); |
|
const segmentAnalysis = data.choices[0].message.content.trim(); |
|
|
|
|
|
analysisSegments.push({ |
|
source: sourceSegments[i], |
|
target: targetSegments[i], |
|
analysis: segmentAnalysis |
|
}); |
|
|
|
|
|
if (i > 0) combinedAnalysis += '\n\n---\n\n'; |
|
combinedAnalysis += `[الجزء ${i+1}]\n${segmentAnalysis}`; |
|
} |
|
|
|
progressDiv.remove(); |
|
return combinedAnalysis; |
|
} |
|
|
|
|
|
|
|
|
|
async function displayDraftSections() { |
|
const container = document.getElementById('paragraphDivisionsContainer'); |
|
container.innerHTML = ''; |
|
|
|
|
|
for (let i = 0; i < analysisSegments.length; i++) { |
|
const segment = analysisSegments[i]; |
|
|
|
|
|
const syncWord = await findSyncWord(segment.source); |
|
|
|
|
|
const sectionDiv = document.createElement('div'); |
|
sectionDiv.className = 'collapsible-section'; |
|
|
|
|
|
const headerDiv = document.createElement('div'); |
|
headerDiv.className = 'section-header'; |
|
headerDiv.innerHTML = ` |
|
<div class="flex items-center"> |
|
<span class="draft-marker">مسودة</span> |
|
<span>القسم ${i+1}: ${truncateText(segment.source, 50)}</span> |
|
</div> |
|
<i class="fas fa-chevron-down"></i> |
|
`; |
|
|
|
|
|
const contentDiv = document.createElement('div'); |
|
contentDiv.className = 'section-content'; |
|
|
|
|
|
const sourceWithSync = highlightSyncWord(segment.source, syncWord); |
|
|
|
const targetWithSync = highlightSyncWord(segment.target, syncWord); |
|
|
|
contentDiv.innerHTML = ` |
|
<div class="paragraph-section mb-4"> |
|
<h4 class="font-bold text-gray-700 mb-2">النص المصدر:</h4> |
|
<div class="bg-blue-50 p-3 rounded-lg">${sourceWithSync}</div> |
|
</div> |
|
<div class="paragraph-section mb-4"> |
|
<h4 class="font-bold text-gray-700 mb-2">النص الهدف:</h4> |
|
<div class="bg-blue-50 p-3 rounded-lg">${targetWithSync}</div> |
|
</div> |
|
<div class="paragraph-section"> |
|
<h4 class="font-bold text-gray-700 mb-2">التحليل:</h4> |
|
<div class="bg-gray-50 p-3 rounded-lg">${formatAnalysisText(segment.analysis)}</div> |
|
</div> |
|
`; |
|
|
|
|
|
sectionDiv.appendChild(headerDiv); |
|
sectionDiv.appendChild(contentDiv); |
|
container.appendChild(sectionDiv); |
|
|
|
|
|
headerDiv.addEventListener('click', function() { |
|
contentDiv.classList.toggle('open'); |
|
const icon = headerDiv.querySelector('i.fas'); |
|
icon.classList.toggle('fa-chevron-down'); |
|
icon.classList.toggle('fa-chevron-up'); |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function displaySegmentedView() { |
|
const container = document.getElementById('segmentedComparisonContainer'); |
|
container.innerHTML = ''; |
|
|
|
|
|
if (!analysisSegments || analysisSegments.length === 0) { |
|
container.innerHTML = '<div class="p-4 bg-yellow-50 rounded-lg text-center">لم يتم تحليل أي مقاطع بعد. قم بتحليل النصوص أولاً.</div>'; |
|
return; |
|
} |
|
|
|
|
|
for (let i = 0; i < analysisSegments.length; i++) { |
|
const segment = analysisSegments[i]; |
|
|
|
|
|
const hasNumbers = segment.analysis.includes('<') && segment.analysis.includes('>'); |
|
const hasMissing = segment.analysis.includes('__'); |
|
const hasMeaning = segment.analysis.includes('[MEANING]'); |
|
|
|
|
|
let tagHTML = ''; |
|
if (hasNumbers) { |
|
tagHTML += '<span class="segment-tag tag-error">أخطاء أرقام</span>'; |
|
} |
|
if (hasMissing) { |
|
tagHTML += '<span class="segment-tag tag-warning">نصوص مفقودة</span>'; |
|
} |
|
if (hasMeaning) { |
|
tagHTML += '<span class="segment-tag tag-info">اختلاف معنى</span>'; |
|
} |
|
if (!hasNumbers && !hasMissing && !hasMeaning) { |
|
tagHTML = '<span class="segment-tag" style="background-color: #d1fae5; color: #065f46;">مطابق</span>'; |
|
} |
|
|
|
|
|
const segmentDiv = document.createElement('div'); |
|
segmentDiv.className = 'segment-comparison'; |
|
|
|
|
|
const sourceHighlighted = applyHighlights(segment.source, segment.analysis); |
|
const targetHighlighted = applyHighlights(segment.target, segment.analysis); |
|
|
|
segmentDiv.innerHTML = ` |
|
<div class="segment-header"> |
|
<div>المقطع ${i+1}</div> |
|
<div>${tagHTML}</div> |
|
</div> |
|
<div class="segment-content"> |
|
<div class="segment-source">${sourceHighlighted}</div> |
|
<div class="segment-target">${targetHighlighted}</div> |
|
</div> |
|
<div class="segment-notes"> |
|
<strong>ملاحظات:</strong> ${formatAnalysisText(segment.analysis)} |
|
</div> |
|
`; |
|
|
|
container.appendChild(segmentDiv); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function setupInteractiveView() { |
|
|
|
allDifferences = []; |
|
let numberDiffCount = 0; |
|
let missingTextCount = 0; |
|
let meaningDiffCount = 0; |
|
|
|
|
|
analysisSegments.forEach((segment, segmentIndex) => { |
|
|
|
const numberMatches = Array.from(segment.analysis.matchAll(/<([^<>]+)>/g)); |
|
numberMatches.forEach(match => { |
|
allDifferences.push({ |
|
type: 'number', |
|
text: match[1], |
|
segmentIndex: segmentIndex, |
|
context: getContextAroundMatch(segment.source, match[1], 10) |
|
}); |
|
numberDiffCount++; |
|
}); |
|
|
|
|
|
const missingMatches = Array.from(segment.analysis.matchAll(/__(.*?)__/g)); |
|
missingMatches.forEach(match => { |
|
allDifferences.push({ |
|
type: 'missing', |
|
text: match[1], |
|
segmentIndex: segmentIndex, |
|
context: getContextAroundMatch(segment.source, match[1], 10) |
|
}); |
|
missingTextCount++; |
|
}); |
|
|
|
|
|
const meaningMatches = Array.from(segment.analysis.matchAll(/\[MEANING\](.*?)\[\/MEANING\]/g)); |
|
meaningMatches.forEach(match => { |
|
allDifferences.push({ |
|
type: 'meaning', |
|
text: match[1], |
|
segmentIndex: segmentIndex, |
|
context: getContextAroundMatch(segment.source, match[1], 10) |
|
}); |
|
meaningDiffCount++; |
|
}); |
|
}); |
|
|
|
|
|
document.getElementById('numberDiffCount').textContent = numberDiffCount; |
|
document.getElementById('missingTextCount').textContent = missingTextCount; |
|
document.getElementById('meaningDiffCount').textContent = meaningDiffCount; |
|
|
|
|
|
updateNavigationButtons(); |
|
|
|
|
|
const recommendationsContainer = document.getElementById('diffRecommendations'); |
|
recommendationsContainer.innerHTML = ''; |
|
|
|
if (allDifferences.length === 0) { |
|
recommendationsContainer.innerHTML = '<p>لا توجد اختلافات تحتاج إلى معالجة!</p>'; |
|
} else { |
|
|
|
if (numberDiffCount > 0) { |
|
recommendationsContainer.innerHTML += `<p>• راجع الأرقام واحرص على تطابقها بين النصين.</p>`; |
|
} |
|
if (missingTextCount > 0) { |
|
recommendationsContainer.innerHTML += `<p>• أضف النصوص المفقودة في الترجمة لضمان اكتمال المحتوى.</p>`; |
|
} |
|
if (meaningDiffCount > 0) { |
|
recommendationsContainer.innerHTML += `<p>• صحح اختلافات المعنى لضمان دقة الترجمة.</p>`; |
|
} |
|
|
|
|
|
recommendationsContainer.innerHTML += `<p>• استخدم وضع العرض المقسم للتعديل الدقيق.</p>`; |
|
} |
|
|
|
|
|
document.getElementById('prevDiff').addEventListener('click', showPreviousDifference); |
|
document.getElementById('nextDiff').addEventListener('click', showNextDifference); |
|
|
|
|
|
document.getElementById('diffCounter').textContent = allDifferences.length > 0 ? |
|
`0/${allDifferences.length}` : "0/0"; |
|
} |
|
|
|
|
|
function getContextAroundMatch(text, match, contextSize) { |
|
const index = text.indexOf(match); |
|
if (index === -1) return ""; |
|
|
|
const start = Math.max(0, index - contextSize); |
|
const end = Math.min(text.length, index + match.length + contextSize); |
|
|
|
let context = text.substring(start, end); |
|
if (start > 0) context = '...' + context; |
|
if (end < text.length) context = context + '...'; |
|
|
|
return context; |
|
} |
|
|
|
|
|
function showNextDifference() { |
|
if (allDifferences.length === 0) return; |
|
|
|
currentDiffIndex = (currentDiffIndex + 1) % allDifferences.length; |
|
displayCurrentDifference(); |
|
updateNavigationButtons(); |
|
} |
|
|
|
|
|
function showPreviousDifference() { |
|
if (allDifferences.length === 0) return; |
|
|
|
currentDiffIndex = (currentDiffIndex - 1 + allDifferences.length) % allDifferences.length; |
|
displayCurrentDifference(); |
|
updateNavigationButtons(); |
|
} |
|
|
|
|
|
function displayCurrentDifference() { |
|
if (allDifferences.length === 0 || currentDiffIndex < 0) { |
|
document.getElementById('currentDiffDisplay').innerHTML = |
|
'<p class="text-gray-500 text-center">اضغط "التالي" للبدء في استعراض الاختلافات</p>'; |
|
document.getElementById('diffCounter').textContent = "0/0"; |
|
return; |
|
} |
|
|
|
const diff = allDifferences[currentDiffIndex]; |
|
const segment = analysisSegments[diff.segmentIndex]; |
|
|
|
let typeLabel = ''; |
|
let typeClass = ''; |
|
let icon = ''; |
|
|
|
if (diff.type === 'number') { |
|
typeLabel = 'اختلاف رقمي'; |
|
typeClass = 'bg-yellow-100 text-yellow-800'; |
|
icon = 'fas fa-hashtag'; |
|
} else if (diff.type === 'missing') { |
|
typeLabel = 'نص مفقود'; |
|
typeClass = 'bg-blue-100 text-blue-800'; |
|
icon = 'fas fa-minus-circle'; |
|
} else if (diff.type === 'meaning') { |
|
typeLabel = 'اختلاف معنى'; |
|
typeClass = 'bg-red-100 text-red-800'; |
|
icon = 'fas fa-exclamation-circle'; |
|
} |
|
|
|
let highlightedContext = diff.context; |
|
highlightedContext = highlightedContext.replace(new RegExp(escapeRegExp(diff.text), 'g'), |
|
`<mark class="${diff.type === 'number' ? 'highlight-number' : diff.type === 'missing' ? 'highlight-missing' : 'highlight-meaning'}">${diff.text}</mark>`); |
|
|
|
document.getElementById('currentDiffDisplay').innerHTML = ` |
|
<div class="mb-3 flex items-center justify-between"> |
|
<div class="flex items-center"> |
|
<span class="rounded-full px-3 py-1 text-sm ${typeClass} flex items-center"> |
|
<i class="${icon} ml-1"></i> |
|
${typeLabel} |
|
</span> |
|
<span class="mr-2 text-gray-500">المقطع ${diff.segmentIndex + 1}</span> |
|
</div> |
|
<span class="text-sm text-gray-500">${currentDiffIndex + 1}/${allDifferences.length}</span> |
|
</div> |
|
<div class="mb-3 font-bold">الاختلاف:</div> |
|
<div class="bg-gray-50 p-3 rounded-lg mb-3">${highlightedContext}</div> |
|
<div class="text-sm text-gray-600"> |
|
<div class="font-bold mb-1">التوصية:</div> |
|
${getRecommendationForDiff(diff)} |
|
</div> |
|
`; |
|
|
|
document.getElementById('diffCounter').textContent = `${currentDiffIndex + 1}/${allDifferences.length}`; |
|
} |
|
|
|
|
|
function getRecommendationForDiff(diff) { |
|
if (diff.type === 'number') { |
|
return 'تأكد من صحة الرقم في النص الهدف وتطابقه مع النص المصدر.'; |
|
} else if (diff.type === 'missing') { |
|
return 'أضف النص المفقود إلى الترجمة للحفاظ على اكتمال المعنى.'; |
|
} else if (diff.type === 'meaning') { |
|
return 'راجع الترجمة للتأكد من نقل المعنى الصحيح دون تحريف.'; |
|
} |
|
return ''; |
|
} |
|
|
|
|
|
function updateNavigationButtons() { |
|
document.getElementById('prevDiff').disabled = allDifferences.length === 0; |
|
document.getElementById('nextDiff').disabled = allDifferences.length === 0; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function countWords(text) { |
|
return text.trim().split(/\s+/).filter(word => word !== "").length; |
|
} |
|
|
|
function escapeRegExp(string) { |
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
|
} |
|
|
|
function splitIntoLines(text) { |
|
return text.split('\n').map((line, i) => `<div class="line-item"><span class="line-number">${i+1}:</span> <span class="line-text">${line}</span></div>`).join(''); |
|
} |
|
|
|
function getLineNumber(text, substring) { |
|
const index = text.indexOf(substring); |
|
if (index === -1) return "غير محدد"; |
|
return text.substring(0, index).split("\n").length; |
|
} |
|
|
|
function truncateText(text, maxLength) { |
|
if (text.length <= maxLength) return text; |
|
return text.substring(0, maxLength) + '...'; |
|
} |
|
|
|
function formatAnalysisText(text) { |
|
|
|
text = text.replace(/الأرقام/g, '<span class="font-bold text-blue-600">الأرقام</span>'); |
|
text = text.replace(/المفقودة/g, '<span class="font-bold text-blue-600">المفقودة</span>'); |
|
text = text.replace(/المعنى/g, '<span class="font-bold text-blue-600">المعنى</span>'); |
|
|
|
|
|
text = text.replace(/<([^<>]+)>/g, '<span class="highlight-number"><$1></span>'); |
|
text = text.replace(/__(.*?)__/g, '<span class="highlight-missing">__$1__</span>'); |
|
text = text.replace(/\[MEANING\](.*?)\[\/MEANING\]/g, '<span class="highlight-meaning">$1</span>'); |
|
|
|
return text; |
|
} |
|
|
|
function highlightSyncWord(text, syncWord) { |
|
if (!syncWord || !text.includes(syncWord)) return text; |
|
|
|
|
|
const regex = new RegExp(`(\\b${escapeRegExp(syncWord)}\\b)`, 'g'); |
|
|
|
const lastIndex = text.lastIndexOf(syncWord); |
|
if (lastIndex !== -1) { |
|
const beforeSync = text.substring(0, lastIndex); |
|
const afterSync = text.substring(lastIndex + syncWord.length); |
|
return beforeSync + '<span class="sync-word">' + syncWord + '</span>' + afterSync; |
|
} |
|
|
|
return text; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function applyHighlights(originalText, analysisOutput) { |
|
|
|
const numberMatches = Array.from(analysisOutput.matchAll(/<([^<>]+)>/g)).map(m => m[1].trim()); |
|
const missingMatches = Array.from(analysisOutput.matchAll(/__(.*?)__/g)).map(m => m[1].trim()); |
|
const meaningMatches = Array.from(analysisOutput.matchAll(/\[MEANING\](.*?)\[\/MEANING\]/g)).map(m => m[1].trim()); |
|
|
|
|
|
const lines = originalText.split('\n'); |
|
|
|
|
|
const highlightedLines = lines.map(line => { |
|
|
|
numberMatches.forEach(phrase => { |
|
if (line.includes(phrase)) { |
|
|
|
const normalizedPhrase = normalizeNumbers(phrase); |
|
const normalizedLine = normalizeNumbers(line); |
|
|
|
if (normalizedLine.includes(normalizedPhrase)) { |
|
|
|
} else { |
|
const regex = new RegExp(escapeRegExp(phrase), 'g'); |
|
line = line.replace(regex, `<span class="highlight-number">${phrase}</span>`); |
|
} |
|
} |
|
}); |
|
|
|
|
|
missingMatches.forEach(phrase => { |
|
if (line.includes(phrase)) { |
|
const regex = new RegExp(escapeRegExp(phrase), 'g'); |
|
line = line.replace(regex, `<span class="highlight-missing">__${phrase}__</span>`); |
|
} |
|
}); |
|
|
|
meaningMatches.forEach(phrase => { |
|
if (line.includes(phrase)) { |
|
const regex = new RegExp(escapeRegExp(phrase), 'g'); |
|
line = line.replace(regex, `<span class="highlight-meaning">${phrase}</span>`); |
|
} |
|
}); |
|
return line; |
|
}); |
|
|
|
return highlightedLines.join('\n'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function generateExplanation(sourceText, analysisOutput) { |
|
let steps = []; |
|
|
|
const missingRegex = /__(.*?)__/g; |
|
let match; |
|
while ((match = missingRegex.exec(analysisOutput)) !== null) { |
|
const phrase = match[1].trim(); |
|
if (phrase) { |
|
const lineNum = getLineNumber(sourceText, phrase); |
|
steps.push(`<li><strong>الخطوة 1:</strong> في السطر ${lineNum}، الجزء "<span class="highlight-missing">__${phrase}__</span>" من النص المصدر مفقود في النص الهدف. تأكد من إضافته لتحسين الدقة.</li>`); |
|
} |
|
} |
|
|
|
const numberRegex = /<([^<>]+)>/g; |
|
while ((match = numberRegex.exec(analysisOutput)) !== null) { |
|
const phrase = match[1].trim(); |
|
if (phrase) { |
|
const lineNum = getLineNumber(sourceText, phrase); |
|
steps.push(`<li><strong>الخطوة 2:</strong> في السطر ${lineNum}، الرقم "<span class="highlight-number">${phrase}</span>" في المصدر لا يتطابق مع الرقم في الهدف. يرجى المراجعة.</li>`); |
|
} |
|
} |
|
|
|
const meaningRegex = /\[MEANING\](.*?)\[\/MEANING\]/g; |
|
while ((match = meaningRegex.exec(analysisOutput)) !== null) { |
|
const phrase = match[1].trim(); |
|
if (phrase) { |
|
const lineNum = getLineNumber(sourceText, phrase); |
|
steps.push(`<li><strong>الخطوة 3:</strong> في السطر ${lineNum}، تم العثور على اختلاف في المعنى مع التعبير "<span class="highlight-meaning">${phrase}</span>". تحقق من الدقة.</li>`); |
|
} |
|
} |
|
|
|
if (steps.length === 0) { |
|
return `<p>النصوص متطابقة تماماً ولا توجد فروقات تحتاج للتنبيه.</p>`; |
|
} |
|
return `<ol class="list-decimal ml-6 space-y-2">${steps.join('')}</ol>`; |
|
} |
|
|
|
|
|
|
|
|
|
async function processFile(file) { |
|
let text = ""; |
|
if (file.type === 'application/pdf') { |
|
try { |
|
|
|
const arrayBuffer = await file.arrayBuffer(); |
|
const loadingTask = pdfjsLib.getDocument(arrayBuffer); |
|
const pdf = await loadingTask.promise; |
|
|
|
const numPages = pdf.numPages; |
|
let fullText = ""; |
|
|
|
for (let i = 1; i <= numPages; i++) { |
|
const page = await pdf.getPage(i); |
|
const content = await page.getTextContent(); |
|
const pageText = content.items.map(item => item.str).join(' '); |
|
fullText += pageText + "\n\n"; |
|
} |
|
|
|
text = fullText; |
|
} catch (error) { |
|
console.error('خطأ في استخراج النص من PDF:', error); |
|
throw new Error('فشل استخراج النص من ملف PDF'); |
|
} |
|
} else if (file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') { |
|
const arrayBuffer = await file.arrayBuffer(); |
|
const result = await mammoth.extractRawText({ arrayBuffer }); |
|
text = result.value; |
|
} else { |
|
throw new Error('نوع الملف غير مدعوم'); |
|
} |
|
return text; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('sourceFile')?.addEventListener('change', async (event) => { |
|
const file = event.target.files[0]; |
|
if (!file) return; |
|
|
|
|
|
if (file.type === 'application/pdf' || file.type.startsWith('image/')) { |
|
return; |
|
} |
|
|
|
document.getElementById('processingStatus').classList.remove('hidden'); |
|
try { |
|
const text = await processFile(file); |
|
document.getElementById('sourceText').value = text; |
|
} catch (error) { |
|
console.error('Error processing source file:', error); |
|
addError('خطأ في معالجة ملف السورس'); |
|
} finally { |
|
document.getElementById('processingStatus').classList.add('hidden'); |
|
} |
|
}); |
|
|
|
document.getElementById('targetFile')?.addEventListener('change', async (event) => { |
|
const file = event.target.files[0]; |
|
if (!file) return; |
|
|
|
|
|
if (file.type === 'application/pdf' || file.type.startsWith('image/')) { |
|
return; |
|
} |
|
|
|
document.getElementById('processingStatus').classList.remove('hidden'); |
|
try { |
|
const text = await processFile(file); |
|
document.getElementById('targetText').value = text; |
|
} catch (error) { |
|
console.error('Error processing target file:', error); |
|
addError('خطأ في معالجة ملف التارجت'); |
|
} finally { |
|
document.getElementById('processingStatus').classList.add('hidden'); |
|
} |
|
}); |
|
|
|
document.getElementById('sourceExtraFile')?.addEventListener('change', async (event) => { |
|
const file = event.target.files[0]; |
|
if (!file) return; |
|
document.getElementById('processingStatus').classList.remove('hidden'); |
|
try { |
|
const text = await processFile(file); |
|
document.getElementById('sourceExtraText').value = text; |
|
} catch (error) { |
|
console.error('Error processing extra source file:', error); |
|
addError('خطأ في معالجة ملف المصدر الإضافي'); |
|
} finally { |
|
document.getElementById('processingStatus').classList.add('hidden'); |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
function addError(message, type = 'error') { |
|
const errorsList = document.getElementById('errorsList'); |
|
if (!errorsList) return; |
|
const errorDiv = document.createElement('div'); |
|
errorDiv.className = `p-4 rounded-xl ${type === 'error' ? 'bg-red-50 text-red-700' : type === 'info' ? 'bg-blue-50 text-blue-700' : 'bg-yellow-50 text-yellow-700'}`; |
|
errorDiv.innerHTML = `<div class="flex items-center"> |
|
<i class="fas fa-${type === 'error' ? 'exclamation-circle' : type === 'info' ? 'info-circle' : 'exclamation-triangle'} ml-2 text-${type === 'error' ? 'red' : type === 'info' ? 'blue' : 'yellow'}-500"></i> |
|
<span>${message}</span> |
|
</div>`; |
|
errorsList.appendChild(errorDiv); |
|
} |
|
|
|
|
|
|
|
|
|
document.getElementById('submitBtn').addEventListener('click', async () => { |
|
const sourceText = document.getElementById('sourceText').value; |
|
const targetText = document.getElementById('targetText').value; |
|
|
|
document.getElementById('errorsList').innerHTML = ''; |
|
document.getElementById('resultSection').classList.remove('hidden'); |
|
document.getElementById('explanationBox').classList.remove('hidden'); |
|
|
|
if (!sourceText || !targetText) { |
|
addError('يرجى إدخال كلا النصين المصدر والهدف'); |
|
return; |
|
} |
|
|
|
|
|
const sourceWordCount = countWords(sourceText); |
|
const targetWordCount = countWords(targetText); |
|
if (sourceWordCount !== targetWordCount) { |
|
addError(`عدد كلمات النص المصدر (${sourceWordCount}) يختلف عن النص الهدف (${targetWordCount})`, 'warning'); |
|
} |
|
|
|
try { |
|
|
|
const initialProgressDiv = document.createElement('div'); |
|
initialProgressDiv.className = "bg-blue-100 p-4 rounded-xl mb-4"; |
|
initialProgressDiv.innerHTML = `<div class="flex items-center"> |
|
<div class="animate-spin h-6 w-6 border-4 border-blue-600 rounded-full border-t-transparent ml-3"></div> |
|
<span>جارٍ تقسيم النصوص للتحليل...</span> |
|
</div>`; |
|
document.getElementById('errorsList').appendChild(initialProgressDiv); |
|
|
|
|
|
const sourceSegments = await divideTextWithChatGPT(sourceText); |
|
const targetSegments = await divideTextWithChatGPT(targetText); |
|
|
|
|
|
if (sourceSegments.length !== targetSegments.length) { |
|
|
|
const maxSegments = Math.max(sourceSegments.length, targetSegments.length); |
|
const minSegments = Math.min(sourceSegments.length, targetSegments.length); |
|
|
|
|
|
addError(`تم تعديل تقسيم النصوص لضمان التطابق (${sourceSegments.length} مقابل ${targetSegments.length})`, 'warning'); |
|
|
|
|
|
if (sourceSegments.length < targetSegments.length) { |
|
|
|
const extraSegments = targetSegments.slice(minSegments); |
|
targetSegments.splice(minSegments, extraSegments.length, extraSegments.join('\n\n')); |
|
} else { |
|
|
|
const extraSegments = sourceSegments.slice(minSegments); |
|
sourceSegments.splice(minSegments, extraSegments.length, extraSegments.join('\n\n')); |
|
} |
|
} |
|
|
|
initialProgressDiv.remove(); |
|
|
|
|
|
fullAnalysisText = await analyzeTextSegments(sourceSegments, targetSegments); |
|
|
|
|
|
document.getElementById('sourceTextReview').innerHTML = applyHighlights(sourceText, fullAnalysisText); |
|
document.getElementById('targetTextReview').innerHTML = applyHighlights(targetText, fullAnalysisText); |
|
|
|
|
|
document.getElementById('explanationText').innerHTML = generateExplanation(sourceText, fullAnalysisText); |
|
|
|
|
|
displaySegmentedView(); |
|
|
|
|
|
setupInteractiveView(); |
|
|
|
|
|
await displayDraftSections(); |
|
|
|
} catch (error) { |
|
console.error('Error during analysis:', error); |
|
addError('حدث خطأ أثناء التحليل: ' + error.message); |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.view-tab').forEach(tab => { |
|
tab.addEventListener('click', function() { |
|
|
|
const tabContainer = this.closest('.view-tabs'); |
|
|
|
|
|
tabContainer.querySelectorAll('.view-tab').forEach(t => t.classList.remove('active')); |
|
|
|
this.classList.add('active'); |
|
|
|
|
|
const tabId = this.getAttribute('data-tab'); |
|
|
|
|
|
const contentSelector = tabContainer.closest('div').querySelectorAll('.view-content'); |
|
contentSelector.forEach(content => content.classList.remove('active')); |
|
|
|
|
|
const targetContent = document.getElementById(tabId); |
|
if (targetContent) { |
|
targetContent.classList.add('active'); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
|
|
|
|
document.getElementById('toggleDraftBtn').addEventListener('click', function() { |
|
const draftSection = document.getElementById('fullTextDraftSection'); |
|
draftSection.classList.toggle('hidden'); |
|
}); |
|
|
|
|
|
|
|
|
|
document.getElementById('downloadExcelBtn').addEventListener('click', function() { |
|
|
|
alert('سيتم تنفيذ تنزيل التقرير بصيغة Excel قريبًا'); |
|
}); |
|
|
|
|
|
|
|
|
|
document.getElementById('downloadWordBtn').addEventListener('click', function() { |
|
|
|
alert('سيتم تنفيذ تنزيل التقرير بصيغة Word قريبًا'); |
|
}); |
|
</script> |
|
</body> |
|
</html> |