Spaces:
Running
Running
<html lang="ar" dir="rtl"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>نظام المقارنة المطور - شركة الريحان للترجمة</title> | |
<!-- استيراد مكتبات Tailwind وFont Awesome وMammoth وPDF.js --> | |
<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> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> | |
<style> | |
/* كل أكواد CSS السابقة ظلت كما هي */ | |
/* ================================ | |
تنسيقات الحركات والتأثيرات | |
================================= */ | |
@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); } | |
} | |
@keyframes highlight-pulse { | |
0% { opacity: 0.7; } | |
50% { opacity: 1; } | |
100% { opacity: 0.7; } | |
} | |
/* ================================ | |
تنسيقات النصوص والتحديد المحسنة | |
================================= */ | |
.text-comparison { | |
line-height: 2; | |
white-space: pre-wrap; | |
padding: 1rem; | |
border-radius: 8px; | |
background-color: #fdfdfd; | |
box-shadow: inset 0 0 3px rgba(0,0,0,0.1); | |
} | |
.highlight-number { | |
background-color: #FDE68A; | |
padding: 0 4px; | |
border-radius: 3px; | |
font-weight: bold; | |
cursor: pointer; | |
position: relative; | |
display: inline-block; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
animation: highlight-pulse 3s ease-in-out infinite; | |
border-bottom: 2px solid #F59E0B; | |
} | |
.highlight-missing { | |
background-color: #BFDBFE; | |
padding: 0 4px; | |
border-radius: 3px; | |
font-style: italic; | |
cursor: pointer; | |
position: relative; | |
display: inline-block; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
animation: highlight-pulse 3s ease-in-out infinite; | |
border-bottom: 2px solid #3B82F6; | |
} | |
.highlight-meaning { | |
background-color: #fecaca; | |
color: #B91C1C; | |
padding: 0 4px; | |
border-radius: 3px; | |
font-weight: bold; | |
cursor: pointer; | |
position: relative; | |
display: inline-block; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
animation: highlight-pulse 3s ease-in-out infinite; | |
border-bottom: 2px solid #EF4444; | |
} | |
/* تحسين علامات وأيقونات التحديد - أكثر وضوحا */ | |
.highlight-number::before, | |
.highlight-missing::before, | |
.highlight-meaning::before { | |
content: ""; | |
position: absolute; | |
width: 8px; | |
height: 8px; | |
border-radius: 50%; | |
right: -10px; | |
top: 50%; | |
transform: translateY(-50%); | |
animation: pulse 2s infinite; | |
} | |
.highlight-number::before { | |
background-color: #F59E0B; | |
} | |
.highlight-missing::before { | |
background-color: #3B82F6; | |
} | |
.highlight-meaning::before { | |
background-color: #EF4444; | |
} | |
/* تمييز النص المفقود تماما بشكل مختلف وأكثر وضوحا */ | |
.completely-missing { | |
background-color: #93C5FD; | |
color: #1E3A8A; | |
padding: 2px 8px; | |
border-radius: 4px; | |
margin: 0 2px; | |
font-weight: bold; | |
border-right: 3px solid #2563EB; | |
border-left: 1px solid #2563EB; | |
display: inline-block; | |
position: relative; | |
animation: highlight-pulse 3s ease-in-out infinite; | |
} | |
/* تمييز النص المفقود جزئيا بشكل مختلف وأكثر وضوحا */ | |
.partially-missing { | |
background-color: #DBEAFE; | |
color: #1E40AF; | |
padding: 2px 8px; | |
border-radius: 4px; | |
margin: 0 2px; | |
font-style: italic; | |
border-bottom: 2px dashed #3B82F6; | |
display: inline-block; | |
animation: highlight-pulse 3s ease-in-out infinite; | |
} | |
/* إضافة أيقونات داخل التحديد لتوضيح نوع الخطأ */ | |
.highlight-number::after { | |
content: "١٢٣"; | |
font-size: 8px; | |
position: absolute; | |
top: -8px; | |
left: 0; | |
background: #F59E0B; | |
color: white; | |
padding: 0 3px; | |
border-radius: 3px; | |
font-weight: bold; | |
} | |
.highlight-missing::after { | |
content: "..."; | |
font-size: 8px; | |
position: absolute; | |
top: -8px; | |
left: 0; | |
background: #3B82F6; | |
color: white; | |
padding: 0 3px; | |
border-radius: 3px; | |
font-weight: bold; | |
} | |
.highlight-meaning::after { | |
content: "!"; | |
font-size: 8px; | |
position: absolute; | |
top: -8px; | |
left: 0; | |
background: #EF4444; | |
color: white; | |
padding: 0 3px; | |
border-radius: 3px; | |
font-weight: bold; | |
} | |
/* ================================ | |
تنسيق عرض السطور في المعاينة - محسن | |
================================= */ | |
.split-view { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
gap: 1.5rem; | |
} | |
.line-item { | |
display: flex; | |
align-items: flex-start; | |
margin-bottom: 0.75rem; | |
padding: 0.5rem; | |
border-radius: 4px; | |
transition: background-color 0.2s; | |
} | |
.line-item:hover { | |
background-color: #f1f5f9; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.05); | |
transform: translateY(-1px); | |
} | |
.line-number { | |
width: 40px; | |
font-weight: bold; | |
color: #4B5563; | |
flex-shrink: 0; | |
background-color: #e5e7eb; | |
padding: 0 5px; | |
border-radius: 3px; | |
text-align: center; | |
margin-left: 8px; | |
} | |
.line-text { | |
flex: 1; | |
line-height: 1.8; | |
} | |
/* ================================ | |
تحسين تنسيق البطاقات والعناصر | |
================================= */ | |
.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: 12px; | |
overflow: hidden; | |
background-color: #f9fafb; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
transition: all 0.3s ease; | |
} | |
.collapsible-section:hover { | |
box-shadow: 0 3px 6px rgba(0,0,0,0.08); | |
} | |
.section-header { | |
background-color: #f3f4f6; | |
padding: 14px 16px; | |
cursor: pointer; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
font-weight: 600; | |
border-bottom: 1px solid #e5e7eb; | |
transition: background-color 0.2s; | |
} | |
.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; | |
background-color: rgba(79, 70, 229, 0.1); | |
padding: 0 3px; | |
border-radius: 3px; | |
} | |
.paragraph-section { | |
border-left: 3px solid #d1d5db; | |
padding-left: 16px; | |
margin-bottom: 20px; | |
position: relative; | |
} | |
.paragraph-section::before { | |
content: ""; | |
position: absolute; | |
width: 10px; | |
height: 10px; | |
background-color: #d1d5db; | |
border-radius: 50%; | |
left: -6.5px; | |
top: 0; | |
} | |
/* ================================ | |
تنسيقات جديدة للشعار والعرض المحسن | |
================================= */ | |
.logo-container { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
margin-bottom: 1.5rem; | |
} | |
.logo { | |
width: 90px; | |
height: 90px; | |
border-radius: 50%; | |
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2); | |
margin-left: 1.5rem; | |
position: relative; | |
overflow: hidden; | |
} | |
.logo::after { | |
content: "ر"; | |
font-size: 48px; | |
font-weight: bold; | |
color: white; | |
text-shadow: 1px 1px 3px rgba(0,0,0,0.3); | |
} | |
.logo::before { | |
content: ""; | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
background: radial-gradient(circle at 70% 30%, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0) 60%); | |
} | |
.company-name { | |
font-size: 2rem; | |
font-weight: bold; | |
color: #fff; | |
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); | |
} | |
/* تحسين طريقة العرض المقسمة للنصوص */ | |
.segment-comparison { | |
border: 1px solid #e5e7eb; | |
border-radius: 12px; | |
margin-bottom: 1.5rem; | |
overflow: hidden; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.05); | |
transition: all 0.3s ease; | |
} | |
.segment-comparison:hover { | |
box-shadow: 0 4px 8px rgba(0,0,0,0.1); | |
transform: translateY(-2px); | |
} | |
.segment-header { | |
background-color: #f3f4f6; | |
padding: 1rem 1.25rem; | |
font-weight: 600; | |
border-bottom: 1px solid #e5e7eb; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.segment-content { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 1px; | |
background-color: #e5e7eb; | |
} | |
.segment-source, .segment-target { | |
background-color: #fff; | |
padding: 1.25rem; | |
transition: background-color 0.2s ease; | |
position: relative; | |
} | |
.segment-source { | |
background-color: #f0f9ff; | |
border-left: 3px solid #93c5fd; | |
} | |
.segment-target { | |
background-color: #fdf2f8; | |
border-right: 3px solid #fbcfe8; | |
} | |
.segment-source:hover, .segment-target:hover { | |
background-color: #f8fafc; | |
} | |
.segment-source::after, .segment-target::after { | |
content: ""; | |
position: absolute; | |
bottom: 0; | |
height: 3px; | |
width: 0; | |
transition: width 0.3s ease; | |
} | |
.segment-source::after { | |
background-color: #3b82f6; | |
right: 0; | |
} | |
.segment-target::after { | |
background-color: #ec4899; | |
left: 0; | |
} | |
.segment-source:hover::after, .segment-target:hover::after { | |
width: 100%; | |
} | |
.segment-notes { | |
grid-column: span 2; | |
background-color: #fffbeb; | |
padding: 1rem 1.25rem; | |
border-top: 1px solid #e5e7eb; | |
font-size: 0.95rem; | |
position: relative; | |
} | |
.segment-notes::before { | |
content: "ملاحظات"; | |
position: absolute; | |
top: -10px; | |
right: 20px; | |
background-color: #fbbf24; | |
color: #fff; | |
padding: 2px 10px; | |
border-radius: 10px; | |
font-size: 12px; | |
font-weight: bold; | |
} | |
.segment-reference { | |
grid-column: span 2; | |
background-color: #f0fdf4; | |
padding: 1rem 1.25rem; | |
border-top: 1px solid #e5e7eb; | |
font-size: 0.95rem; | |
color: #166534; | |
position: relative; | |
} | |
.segment-reference::before { | |
content: "مرجع"; | |
position: absolute; | |
top: -10px; | |
right: 20px; | |
background-color: #22c55e; | |
color: #fff; | |
padding: 2px 10px; | |
border-radius: 10px; | |
font-size: 12px; | |
font-weight: bold; | |
} | |
.segment-tag { | |
display: inline-block; | |
padding: 0.35rem 0.75rem; | |
border-radius: 9999px; | |
font-size: 0.8rem; | |
font-weight: 500; | |
margin-right: 0.5rem; | |
position: relative; | |
padding-right: 25px; | |
} | |
.segment-tag::before { | |
content: ""; | |
position: absolute; | |
width: 12px; | |
height: 12px; | |
border-radius: 50%; | |
right: 8px; | |
top: 50%; | |
transform: translateY(-50%); | |
} | |
.tag-error { | |
background-color: #fee2e2; | |
color: #b91c1c; | |
} | |
.tag-error::before { | |
background-color: #ef4444; | |
} | |
.tag-warning { | |
background-color: #fef3c7; | |
color: #92400e; | |
} | |
.tag-warning::before { | |
background-color: #f59e0b; | |
} | |
.tag-info { | |
background-color: #dbeafe; | |
color: #1e40af; | |
} | |
.tag-info::before { | |
background-color: #3b82f6; | |
} | |
.tag-success { | |
background-color: #d1fae5; | |
color: #065f46; | |
} | |
.tag-success::before { | |
background-color: #10b981; | |
} | |
/* ================================ | |
تنسيقات OCR محسنة | |
================================= */ | |
.pdf-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); | |
gap: 12px; | |
margin-top: 20px; | |
} | |
.pdf-page { | |
border: 1px solid #ddd; | |
border-radius: 6px; | |
padding: 8px; | |
position: relative; | |
cursor: pointer; | |
transition: all 0.3s; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
} | |
.pdf-page:hover { | |
transform: translateY(-4px); | |
box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
border-color: #bfdbfe; | |
} | |
.pdf-page.selected { | |
border: 2px solid #3b82f6; | |
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3); | |
} | |
.pdf-page img { | |
width: 100%; | |
height: auto; | |
border-radius: 4px; | |
object-fit: cover; | |
} | |
.page-number { | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
right: 0; | |
background-color: rgba(0,0,0,0.7); | |
color: white; | |
text-align: center; | |
font-size: 12px; | |
padding: 3px; | |
border-bottom-left-radius: 5px; | |
border-bottom-right-radius: 5px; | |
} | |
/* تنسيقات النص المستخرج */ | |
.result-text { | |
max-height: 300px; | |
overflow-y: auto; | |
white-space: pre-wrap; | |
direction: rtl; | |
border: 1px solid #ddd; | |
padding: 1rem; | |
border-radius: 8px; | |
background-color: #f8f9fa; | |
font-size: 1.05rem; | |
line-height: 1.8; | |
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); | |
} | |
.result-text:focus { | |
border-color: #3b82f6; | |
outline: none; | |
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3), inset 0 1px 3px rgba(0,0,0,0.1); | |
} | |
/* تنسيقات معاينة الصفحات */ | |
.page-preview { | |
margin-bottom: 15px; | |
border: 1px solid #ddd; | |
border-radius: 8px; | |
padding: 15px; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
transition: all 0.3s; | |
} | |
.page-preview:hover { | |
box-shadow: 0 3px 6px rgba(0,0,0,0.1); | |
} | |
.page-preview h4 { | |
background-color: #f0f8ff; | |
padding: 8px 12px; | |
border-radius: 6px; | |
margin-bottom: 12px; | |
font-weight: 600; | |
color: #1e40af; | |
border-right: 3px solid #3b82f6; | |
} | |
/* شريط التقدم والمؤشرات */ | |
.progress { | |
height: 0.5rem; | |
border-radius: 9999px; | |
overflow: hidden; | |
background-color: #e5e7eb; | |
} | |
.progress-bar { | |
height: 100%; | |
border-radius: 9999px; | |
background: linear-gradient(90deg, #3b82f6, #2563eb); | |
transition: width 0.4s ease; | |
} | |
.progress-bar.success { | |
background: linear-gradient(90deg, #10b981, #059669); | |
} | |
.progress-bar.warning { | |
background: linear-gradient(90deg, #f59e0b, #d97706); | |
} | |
.progress-bar.error { | |
background: linear-gradient(90deg, #ef4444, #dc2626); | |
} | |
/* تنسيقات الإحصائيات */ | |
.stats-badge { | |
background: linear-gradient(135deg, #3b82f6, #1e40af); | |
color: white; | |
font-size: 14px; | |
padding: 6px 12px; | |
border-radius: 20px; | |
margin-right: 10px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
transition: all 0.3s; | |
position: relative; | |
overflow: hidden; | |
} | |
.stats-badge::before { | |
content: ""; | |
position: absolute; | |
top: 0; | |
left: -50%; | |
width: 150%; | |
height: 100%; | |
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); | |
transform: skewX(-25deg); | |
animation: shine 2s infinite; | |
} | |
@keyframes shine { | |
0% { left: -50%; } | |
100% { left: 150%; } | |
} | |
.stats-badge:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 4px 8px rgba(0,0,0,0.15); | |
} | |
.stats-container { | |
display: flex; | |
flex-wrap: wrap; | |
align-items: center; | |
margin-bottom: 20px; | |
gap: 10px; | |
} | |
/* تنسيقات خاصة بترجمة الريحان */ | |
.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); | |
border-radius: 8px; | |
} | |
/* تنسيقات جديدة لشرح الأخطاء */ | |
.error-popup { | |
position: fixed; | |
bottom: 20px; | |
right: 20px; | |
max-width: 450px; | |
width: 90%; | |
background-color: white; | |
border-radius: 12px; | |
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.25); | |
z-index: 1000; | |
overflow: hidden; | |
display: none; /* Hidden by default */ | |
transform: translateY(10px); | |
opacity: 0; | |
transition: transform 0.3s ease, opacity 0.3s ease; | |
} | |
.error-popup.show { | |
transform: translateY(0); | |
opacity: 1; | |
} | |
.error-popup-header { | |
background: linear-gradient(135deg, #3b82f6, #1e40af); | |
color: white; | |
padding: 12px 18px; | |
font-weight: bold; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.error-popup-body { | |
padding: 18px; | |
max-height: 300px; | |
overflow-y: auto; | |
} | |
.error-popup-close { | |
cursor: pointer; | |
background: none; | |
border: none; | |
color: white; | |
font-size: 20px; | |
transition: transform 0.2s; | |
} | |
.error-popup-close:hover { | |
transform: scale(1.2); | |
} | |
/* تنسيقات جديدة لتمييز الجملة التي بها خطأ */ | |
.sentence-with-error { | |
border-bottom: 2px dotted #b91c1c; | |
padding-bottom: 3px; | |
position: relative; | |
} | |
.sentence-with-error:hover::after { | |
content: "انقر لرؤية التفاصيل"; | |
position: absolute; | |
bottom: -25px; | |
right: 0; | |
background-color: #fef2f2; | |
color: #b91c1c; | |
padding: 3px 8px; | |
border-radius: 4px; | |
font-size: 12px; | |
z-index: 10; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.15); | |
} | |
/* تنسيقات قسم توضيح الأخطاء */ | |
.explanation-panel { | |
background-color: #f8fafc; | |
border: 1px solid #e2e8f0; | |
border-radius: 12px; | |
padding: 20px; | |
margin-top: 24px; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
position: relative; | |
} | |
.explanation-panel::before { | |
content: ""; | |
position: absolute; | |
top: -10px; | |
right: 30px; | |
width: 20px; | |
height: 20px; | |
background-color: #f8fafc; | |
border-top: 1px solid #e2e8f0; | |
border-right: 1px solid #e2e8f0; | |
transform: rotate(-45deg); | |
} | |
.explanation-panel h5 { | |
font-weight: 600; | |
margin-bottom: 12px; | |
color: #1e40af; | |
font-size: 1.1rem; | |
border-bottom: 2px solid #dbeafe; | |
padding-bottom: 8px; | |
} | |
/* ================================ | |
تنسيقات طرق العرض المنفصلة | |
================================= */ | |
.view-container { | |
margin-top: 1.5rem; | |
display: none; /* مخفي افتراضيًا */ | |
animation: fadeIn 0.5s; | |
} | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(10px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
.view-container.active { | |
display: block; | |
} | |
.view-selector { | |
display: flex; | |
gap: 1rem; | |
margin-bottom: 1.5rem; | |
flex-wrap: wrap; | |
} | |
.view-btn { | |
padding: 0.75rem 1.5rem; | |
background-color: #e5e7eb; | |
border-radius: 10px; | |
font-weight: 500; | |
cursor: pointer; | |
transition: all 0.3s; | |
display: flex; | |
align-items: center; | |
white-space: nowrap; | |
box-shadow: 0 1px 2px rgba(0,0,0,0.05); | |
position: relative; | |
overflow: hidden; | |
} | |
.view-btn:hover { | |
background-color: #d1d5db; | |
transform: translateY(-2px); | |
box-shadow: 0 3px 6px rgba(0,0,0,0.1); | |
} | |
.view-btn.active { | |
background: linear-gradient(135deg, #3b82f6, #2563eb); | |
color: white; | |
box-shadow: 0 3px 8px rgba(59, 130, 246, 0.3); | |
} | |
.view-btn.active::after { | |
content: ""; | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
width: 100%; | |
height: 3px; | |
background-color: rgba(255,255,255,0.5); | |
} | |
.view-btn i { | |
margin-left: 10px; | |
font-size: 1.1rem; | |
} | |
/* ================================ | |
تنسيقات فلتر الأخطاء - محسنة | |
================================= */ | |
.error-filters { | |
display: flex; | |
gap: 0.75rem; | |
margin-bottom: 1.5rem; | |
flex-wrap: wrap; | |
} | |
.error-filter { | |
padding: 0.6rem 1.2rem; | |
border-radius: 10px; | |
font-size: 0.95rem; | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
transition: all 0.3s; | |
box-shadow: 0 1px 2px rgba(0,0,0,0.05); | |
} | |
.error-filter.active { | |
border-width: 2px; | |
transform: translateY(-2px); | |
box-shadow: 0 3px 8px rgba(0,0,0,0.1); | |
} | |
.error-filter i { | |
margin-left: 8px; | |
font-size: 1.1rem; | |
} | |
.filter-all { | |
background-color: #f3f4f6; | |
border: 1px solid #d1d5db; | |
color: #4b5563; | |
} | |
.filter-all.active { | |
background-color: #e5e7eb; | |
border-color: #9ca3af; | |
color: #1f2937; | |
} | |
.filter-numbers { | |
background-color: #fef3c7; | |
border: 1px solid #fcd34d; | |
color: #92400e; | |
} | |
.filter-numbers.active { | |
background-color: #fde68a; | |
border-color: #f59e0b; | |
} | |
.filter-missing { | |
background-color: #dbeafe; | |
border: 1px solid #93c5fd; | |
color: #1e40af; | |
} | |
.filter-missing.active { | |
background-color: #bfdbfe; | |
border-color: #3b82f6; | |
} | |
.filter-meaning { | |
background-color: #fee2e2; | |
border: 1px solid #fca5a5; | |
color: #b91c1c; | |
} | |
.filter-meaning.active { | |
background-color: #fecaca; | |
border-color: #ef4444; | |
} | |
/* تحسينات على العرض الكلاسيكي */ | |
.classic-view-improved .text-comparison { | |
background-color: #fff; | |
border: 1px solid #e5e7eb; | |
border-radius: 12px; | |
padding: 1.5rem; | |
line-height: 2.2; | |
font-size: 1.1rem; | |
transition: all 0.3s ease; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
} | |
.classic-view-improved .text-comparison:hover { | |
box-shadow: 0 5px 10px rgba(0,0,0,0.08); | |
} | |
.classic-view-improved h4 { | |
position: relative; | |
padding-right: 1.5rem; | |
margin-bottom: 1.2rem; | |
display: inline-block; | |
font-weight: 600; | |
} | |
.classic-view-improved h4::before { | |
content: ""; | |
position: absolute; | |
right: 0; | |
top: 50%; | |
transform: translateY(-50%); | |
width: 0.85rem; | |
height: 0.85rem; | |
border-radius: 50%; | |
} | |
.classic-view-improved .source-title::before { | |
background-color: #3b82f6; | |
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); | |
} | |
.classic-view-improved .target-title::before { | |
background-color: #ec4899; | |
box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.2); | |
} | |
/* تحسينات للتوافق بين الفقرات */ | |
.aligned-paragraphs { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 2px; | |
background-color: #e5e7eb; | |
border-radius: 10px; | |
overflow: hidden; | |
margin-bottom: 1.25rem; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
} | |
.paragraph-ar, .paragraph-en { | |
background-color: #fff; | |
padding: 1.2rem; | |
white-space: pre-wrap; | |
transition: background-color 0.2s; | |
line-height: 1.8; | |
} | |
.paragraph-ar:hover, .paragraph-en:hover { | |
background-color: #f9fafb; | |
} | |
.paragraph-ar { | |
background-color: #f0f9ff; | |
direction: rtl; | |
border-left: 3px solid #bfdbfe; | |
position: relative; | |
} | |
.paragraph-ar::after { | |
content: "المصدر"; | |
position: absolute; | |
top: 0; | |
right: 0; | |
background-color: #3b82f6; | |
color: white; | |
padding: 2px 8px; | |
border-bottom-left-radius: 6px; | |
font-size: 10px; | |
font-weight: bold; | |
} | |
.paragraph-en { | |
background-color: #fdf2f8; | |
direction: ltr; | |
border-right: 3px solid #fbcfe8; | |
position: relative; | |
} | |
.paragraph-en::after { | |
content: "الهدف"; | |
position: absolute; | |
top: 0; | |
left: 0; | |
background-color: #ec4899; | |
color: white; | |
padding: 2px 8px; | |
border-bottom-right-radius: 6px; | |
font-size: 10px; | |
font-weight: bold; | |
} | |
/* ================================ | |
تنسيقات العرض التفاعلي المُحسّن | |
================================= */ | |
.diff-source-target { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 1.5rem; | |
margin-top: 1.5rem; | |
} | |
.diff-panel { | |
background-color: #fff; | |
border: 1px solid #e5e7eb; | |
border-radius: 12px; | |
padding: 1.25rem; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
transition: all 0.3s; | |
} | |
.diff-panel:hover { | |
box-shadow: 0 5px 10px rgba(0,0,0,0.08); | |
} | |
.diff-panel-header { | |
font-weight: 600; | |
margin-bottom: 0.75rem; | |
color: #1e40af; | |
display: flex; | |
align-items: center; | |
border-bottom: 2px solid #dbeafe; | |
padding-bottom: 8px; | |
} | |
.diff-panel-header i { | |
margin-left: 0.75rem; | |
font-size: 1.1rem; | |
} | |
.diff-source { | |
background-color: #f0f9ff; | |
border-color: #bfdbfe; | |
} | |
.diff-target { | |
background-color: #fdf2f8; | |
border-color: #fbcfe8; | |
} | |
.diff-reference { | |
background-color: #f0fdf4; | |
border: 1px solid #86efac; | |
border-radius: 12px; | |
padding: 1.25rem; | |
margin-top: 1.5rem; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
transition: all 0.3s; | |
} | |
.diff-reference:hover { | |
box-shadow: 0 5px 10px rgba(0,0,0,0.08); | |
} | |
.reference-header { | |
font-weight: 600; | |
margin-bottom: 0.75rem; | |
color: #166534; | |
display: flex; | |
align-items: center; | |
border-bottom: 2px solid #bbf7d0; | |
padding-bottom: 8px; | |
} | |
.reference-header i { | |
margin-left: 0.75rem; | |
font-size: 1.1rem; | |
} | |
/* ============================= | |
تنسيقات تحديد النص | |
============================= */ | |
/* تحديد مبدئي للأخطاء في المسودة */ | |
.preliminary-highlight-number { | |
background-color: rgba(253, 224, 71, 0.4); | |
padding: 0 4px; | |
border-radius: 3px; | |
position: relative; | |
} | |
.preliminary-highlight-missing { | |
background-color: rgba(147, 197, 253, 0.4); | |
padding: 0 4px; | |
border-radius: 3px; | |
position: relative; | |
} | |
.preliminary-highlight-meaning { | |
background-color: rgba(252, 165, 165, 0.4); | |
padding: 0 4px; | |
border-radius: 3px; | |
position: relative; | |
} | |
/* ملخص التحليل محسن */ | |
.analysis-summary { | |
background-color: #fff; | |
border-radius: 12px; | |
box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
overflow: hidden; | |
margin-bottom: 1.5rem; | |
} | |
.analysis-summary-header { | |
background: linear-gradient(135deg, #3b82f6, #2563eb); | |
color: white; | |
padding: 16px 20px; | |
font-weight: bold; | |
font-size: 1.1rem; | |
display: flex; | |
align-items: center; | |
} | |
.analysis-summary-header i { | |
margin-left: 10px; | |
font-size: 1.2rem; | |
} | |
.analysis-summary-body { | |
padding: 20px; | |
} | |
.summary-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
gap: 16px; | |
margin-bottom: 20px; | |
} | |
.summary-card { | |
background-color: #f9fafb; | |
border-radius: 8px; | |
padding: 16px; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
text-align: center; | |
transition: all 0.3s; | |
border: 1px solid #e5e7eb; | |
} | |
.summary-card:hover { | |
transform: translateY(-3px); | |
box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
} | |
.summary-card-header { | |
font-size: 3rem; | |
font-weight: bold; | |
margin-bottom: 8px; | |
} | |
.summary-card-title { | |
font-size: 0.9rem; | |
color: #4b5563; | |
display: flex; | |
align-items: center; | |
} | |
.summary-card-title i { | |
margin-left: 6px; | |
} | |
.error-groups { | |
border-top: 1px solid #e5e7eb; | |
padding-top: 16px; | |
} | |
.error-group { | |
margin-bottom: 20px; | |
} | |
.error-group-header { | |
display: flex; | |
align-items: center; | |
font-weight: bold; | |
margin-bottom: 10px; | |
padding-bottom: 6px; | |
border-bottom: 2px solid #f3f4f6; | |
} | |
.error-group-header i { | |
margin-left: 8px; | |
} | |
.error-item { | |
background-color: #f9fafb; | |
border-radius: 8px; | |
padding: 12px; | |
margin-bottom: 10px; | |
border-right: 3px solid; | |
transition: all 0.2s; | |
} | |
.error-item:hover { | |
transform: translateX(-5px); | |
box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
} | |
.error-item-header { | |
font-weight: bold; | |
margin-bottom: 6px; | |
display: flex; | |
justify-content: space-between; | |
} | |
.error-item-body { | |
background-color: white; | |
padding: 8px; | |
border-radius: 4px; | |
border: 1px solid #e5e7eb; | |
} | |
.error-item-footer { | |
margin-top: 8px; | |
font-size: 0.85rem; | |
color: #6b7280; | |
font-style: italic; | |
} | |
.error-number { | |
border-right-color: #f59e0b; | |
} | |
.error-missing { | |
border-right-color: #3b82f6; | |
} | |
.error-meaning { | |
border-right-color: #ef4444; | |
} | |
/* حقوق الملكية - جديد */ | |
.copyright { | |
text-align: center; | |
padding: 15px 0; | |
font-size: 0.9rem; | |
color: #6b7280; | |
border-top: 1px solid #e5e7eb; | |
margin-top: 40px; | |
} | |
.copyright a { | |
color: #3b82f6; | |
text-decoration: none; | |
} | |
.copyright a:hover { | |
text-decoration: underline; | |
} | |
/* ================================ | |
تنسيقات جديدة لاختيار نوع الملف | |
================================= */ | |
.file-type-selector { | |
background-color: #f8fafc; | |
border: 1px solid #e2e8f0; | |
border-radius: 12px; | |
padding: 1rem; | |
margin-bottom: 1.5rem; | |
box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
} | |
.file-type-options { | |
display: flex; | |
gap: 1rem; | |
margin-top: 0.75rem; | |
flex-wrap: wrap; | |
} | |
.file-type-option { | |
display: flex; | |
align-items: center; | |
padding: 0.75rem 1rem; | |
border: 2px solid #e5e7eb; | |
border-radius: 8px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
background-color: #fff; | |
min-width: 150px; | |
} | |
.file-type-option:hover { | |
border-color: #3b82f6; | |
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.1); | |
} | |
.file-type-option.selected { | |
border-color: #3b82f6; | |
background-color: #eff6ff; | |
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
} | |
.file-type-option input[type="radio"] { | |
width: 16px; | |
height: 16px; | |
margin-left: 8px; | |
accent-color: #3b82f6; | |
} | |
.file-type-option label { | |
cursor: pointer; | |
font-weight: 500; | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
} | |
.file-type-option i { | |
font-size: 1.1rem; | |
color: #6b7280; | |
} | |
.file-type-option.selected i { | |
color: #3b82f6; | |
} | |
/* تنسيقات حالة التصحيح */ | |
.correction-status { | |
background-color: #fef3c7; | |
border: 1px solid #fcd34d; | |
border-radius: 8px; | |
padding: 1rem; | |
margin: 1rem 0; | |
display: none; | |
} | |
.correction-status.active { | |
display: block; | |
animation: fadeIn 0.5s ease-in-out; | |
} | |
.correction-status h4 { | |
color: #92400e; | |
margin-bottom: 0.5rem; | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
} | |
.correction-status p { | |
color: #78350f; | |
margin-bottom: 0.5rem; | |
} | |
.correction-progress { | |
background-color: #fbbf24; | |
height: 4px; | |
border-radius: 2px; | |
overflow: hidden; | |
margin-top: 0.5rem; | |
} | |
.correction-progress-bar { | |
background-color: #f59e0b; | |
height: 100%; | |
width: 0%; | |
transition: width 0.3s ease; | |
} | |
/* تنسيقات معاينة التصحيح */ | |
.correction-preview { | |
background-color: #f0fdf4; | |
border: 1px solid #86efac; | |
border-radius: 8px; | |
padding: 1rem; | |
margin: 1rem 0; | |
display: none; | |
} | |
.correction-preview.active { | |
display: block; | |
animation: fadeIn 0.5s ease-in-out; | |
} | |
.correction-preview h4 { | |
color: #166534; | |
margin-bottom: 0.75rem; | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
} | |
.correction-changes { | |
background-color: #fff; | |
border: 1px solid #d1d5db; | |
border-radius: 6px; | |
padding: 0.75rem; | |
max-height: 200px; | |
overflow-y: auto; | |
font-family: monospace; | |
font-size: 0.9rem; | |
line-height: 1.4; | |
} | |
.corrected-text { | |
background-color: #dcfce7; | |
color: #166534; | |
padding: 2px 4px; | |
border-radius: 3px; | |
font-weight: 500; | |
} | |
.original-text { | |
background-color: #fee2e2; | |
color: #b91c1c; | |
padding: 2px 4px; | |
border-radius: 3px; | |
text-decoration: line-through; | |
} | |
/* ================================ | |
تنسيقات جديدة لمؤشر التحميل | |
================================= */ | |
.async-loading-indicator { | |
display: none; | |
padding: 15px; | |
border-radius: 10px; | |
background-color: rgba(255, 255, 255, 0.95); | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
z-index: 1000; | |
text-align: center; | |
min-width: 250px; | |
} | |
.async-loading-indicator.active { | |
display: block; | |
animation: fadeIn 0.3s; | |
} | |
.async-spinner { | |
display: inline-block; | |
width: 50px; | |
height: 50px; | |
border: 5px solid #e5e7eb; | |
border-radius: 50%; | |
border-top-color: #3b82f6; | |
animation: spin 1s linear infinite; | |
margin-bottom: 10px; | |
} | |
@keyframes spin { | |
to { transform: rotate(360deg); } | |
} | |
/* تنسيقات محسنة للنافذة المنبثقة */ | |
.enhanced-popup { | |
position: fixed; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%) scale(0.9); | |
max-width: 600px; | |
width: 90%; | |
background: white; | |
border-radius: 15px; | |
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); | |
z-index: 1100; | |
opacity: 0; | |
visibility: hidden; | |
overflow: hidden; | |
transition: all 0.3s; | |
} | |
.enhanced-popup.show { | |
opacity: 1; | |
visibility: visible; | |
transform: translate(-50%, -50%) scale(1); | |
} | |
.enhanced-popup-header { | |
background: linear-gradient(135deg, #3b82f6, #1e40af); | |
color: white; | |
padding: 20px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.enhanced-popup-title { | |
font-size: 1.25rem; | |
font-weight: bold; | |
display: flex; | |
align-items: center; | |
} | |
.enhanced-popup-title i { | |
margin-left: 10px; | |
font-size: 1.5rem; | |
} | |
.enhanced-popup-close { | |
background: none; | |
border: none; | |
color: white; | |
font-size: 24px; | |
cursor: pointer; | |
transition: transform 0.2s; | |
} | |
.enhanced-popup-close:hover { | |
transform: scale(1.2); | |
} | |
.enhanced-popup-body { | |
padding: 25px; | |
max-height: 70vh; | |
overflow-y: auto; | |
} | |
.enhanced-popup-footer { | |
padding: 15px 25px; | |
background-color: #f8fafc; | |
border-top: 1px solid #e5e7eb; | |
display: flex; | |
justify-content: flex-end; | |
} | |
.popup-overlay { | |
position: fixed; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: rgba(0, 0, 0, 0.5); | |
z-index: 1099; | |
opacity: 0; | |
visibility: hidden; | |
transition: all 0.3s; | |
} | |
.popup-overlay.show { | |
opacity: 1; | |
visibility: visible; | |
} | |
.error-illustration { | |
max-width: 100%; | |
margin: 15px 0; | |
border-radius: 10px; | |
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); | |
} | |
.error-example { | |
background-color: #f8fafc; | |
border: 1px solid #e5e7eb; | |
border-radius: 8px; | |
padding: 15px; | |
margin: 15px 0; | |
font-family: 'Courier New', monospace; | |
} | |
.correct-example { | |
background-color: #f0fdf4; | |
border: 1px solid #86efac; | |
border-radius: 8px; | |
padding: 15px; | |
margin: 15px 0; | |
font-family: 'Courier New', monospace; | |
} | |
/* ================================ | |
1. تنسيقات القوالب الرسمية (جديد) | |
================================= */ | |
.template-selector { | |
margin-top: 1rem; | |
display: none; | |
animation: fadeIn 0.3s ease-in-out; | |
} | |
.template-selector.active { | |
display: block; | |
} | |
.template-selector select { | |
width: 100%; | |
padding: 0.75rem; | |
border: 2px solid #e5e7eb; | |
border-radius: 8px; | |
background-color: white; | |
font-size: 1rem; | |
color: #374151; | |
transition: all 0.3s ease; | |
} | |
.template-selector select:focus { | |
outline: none; | |
border-color: #3b82f6; | |
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); | |
} | |
.template-selector select option { | |
padding: 10px; | |
} | |
.template-info { | |
margin-top: 0.5rem; | |
padding: 0.75rem; | |
background-color: #eff6ff; | |
border-radius: 6px; | |
font-size: 0.9rem; | |
color: #1e40af; | |
} | |
/* ================================ | |
2. تنسيقات إخفاء الشرح/المسودات (جديد) | |
================================= */ | |
.settings-btn { | |
position: fixed; | |
bottom: 20px; | |
left: 20px; | |
width: 50px; | |
height: 50px; | |
border-radius: 50%; | |
background: #3b82f6; | |
color: white; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
cursor: pointer; | |
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); | |
transition: all 0.3s ease; | |
z-index: 999; | |
} | |
.settings-btn:hover { | |
transform: scale(1.1); | |
background: #2563eb; | |
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.25); | |
} | |
.settings-panel { | |
position: fixed; | |
bottom: 80px; | |
left: 20px; | |
width: 280px; | |
background-color: white; | |
border-radius: 12px; | |
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); | |
padding: 16px; | |
z-index: 998; | |
transform: translateY(20px); | |
opacity: 0; | |
visibility: hidden; | |
transition: all 0.3s ease; | |
} | |
.settings-panel.active { | |
transform: translateY(0); | |
opacity: 1; | |
visibility: visible; | |
} | |
.settings-header { | |
display: flex; | |
align-items: center; | |
margin-bottom: 16px; | |
padding-bottom: 8px; | |
border-bottom: 1px solid #e5e7eb; | |
} | |
.settings-header i { | |
margin-left: 8px; | |
color: #3b82f6; | |
font-size: 1.2rem; | |
} | |
.settings-option { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
padding: 8px 0; | |
border-bottom: 1px solid #f3f4f6; | |
} | |
.settings-option:last-child { | |
border-bottom: none; | |
} | |
.switch { | |
position: relative; | |
display: inline-block; | |
width: 44px; | |
height: 24px; | |
} | |
.switch input { | |
opacity: 0; | |
width: 0; | |
height: 0; | |
} | |
.slider { | |
position: absolute; | |
cursor: pointer; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: #e5e7eb; | |
transition: .4s; | |
border-radius: 24px; | |
} | |
.slider:before { | |
position: absolute; | |
content: ""; | |
height: 18px; | |
width: 18px; | |
left: 3px; | |
bottom: 3px; | |
background-color: white; | |
transition: .4s; | |
border-radius: 50%; | |
} | |
input:checked + .slider { | |
background-color: #3b82f6; | |
} | |
input:focus + .slider { | |
box-shadow: 0 0 1px #3b82f6; | |
} | |
input:checked + .slider:before { | |
transform: translateX(20px); | |
} | |
/* ================================ | |
3. تنسيقات زر إعادة تحليل الفقرة (جديد) | |
================================= */ | |
.reanalyze-btn { | |
width: 28px; | |
height: 28px; | |
border-radius: 50%; | |
background-color: #f3f4f6; | |
display: inline-flex; | |
align-items: center; | |
justify-content: center; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
margin-right: 8px; | |
color: #4b5563; | |
} | |
.reanalyze-btn:hover { | |
background-color: #3b82f6; | |
color: white; | |
transform: rotate(180deg); | |
} | |
.reanalyze-btn.spin { | |
animation: spin 1s linear infinite; | |
background-color: #3b82f6; | |
color: white; | |
} | |
/* ================================ | |
4. تنسيقات نمط التركيز (جديد) | |
================================= */ | |
.focus-mode-btn { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
padding: 8px 16px; | |
background-color: #f3f4f6; | |
border-radius: 8px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
margin-right: auto; | |
} | |
.focus-mode-btn:hover { | |
background-color: #e5e7eb; | |
} | |
.focus-mode-btn.active { | |
background-color: #3b82f6; | |
color: white; | |
} | |
.focus-mode-btn i { | |
font-size: 1.1rem; | |
} | |
/* عندما يكون وضع التركيز نشطًا */ | |
body.focus-on .error-filters, | |
body.focus-on .view-selector, | |
body.focus-on .analysis-summary, | |
body.focus-on #toggleDraftBtn, | |
body.focus-on #downloadExcelBtn, | |
body.focus-on #downloadWordBtn, | |
body.focus-on .segment-notes { | |
display: none ; | |
} | |
body.focus-on .segment-comparison { | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
transform: scale(1.01); | |
transition: all 0.5s ease; | |
} | |
body.focus-on .highlight-number, | |
body.focus-on .highlight-missing, | |
body.focus-on .highlight-meaning { | |
animation: highlight-pulse 1.5s ease-in-out infinite; | |
} | |
/* ================================ | |
5. تنسيقات خريطة الحرارة (جديد) | |
================================= */ | |
.heat-bar { | |
position: fixed; | |
right: 10px; | |
top: 50%; | |
transform: translateY(-50%); | |
width: 8px; | |
height: 70vh; | |
background-color: #f3f4f6; | |
border-radius: 4px; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
z-index: 990; | |
} | |
.heat-dot { | |
position: absolute; | |
width: 8px; | |
height: 4px; | |
border-radius: 2px; | |
right: 0; | |
transform: translateY(-50%); | |
cursor: pointer; | |
transition: all 0.3s ease; | |
} | |
.heat-dot:hover { | |
width: 12px; | |
height: 6px; | |
right: -2px; | |
} | |
.heat-dot.number-error { | |
background-color: #f59e0b; | |
} | |
.heat-dot.missing-error { | |
background-color: #3b82f6; | |
} | |
.heat-dot.meaning-error { | |
background-color: #ef4444; | |
} | |
/* Tooltip for heat dots */ | |
.heat-dot::before { | |
content: attr(data-tooltip); | |
position: absolute; | |
top: 50%; | |
right: 16px; | |
transform: translateY(-50%); | |
background-color: rgba(0, 0, 0, 0.8); | |
color: white; | |
padding: 5px 10px; | |
border-radius: 4px; | |
font-size: 12px; | |
white-space: nowrap; | |
opacity: 0; | |
visibility: hidden; | |
transition: all 0.3s ease; | |
z-index: 991; | |
} | |
.heat-dot::after { | |
content: ""; | |
position: absolute; | |
top: 50%; | |
right: 12px; | |
transform: translateY(-50%); | |
border: 4px solid transparent; | |
border-left: 4px solid rgba(0, 0, 0, 0.8); | |
opacity: 0; | |
visibility: hidden; | |
transition: all 0.3s ease; | |
z-index: 991; | |
} | |
.heat-dot:hover::before, | |
.heat-dot:hover::after { | |
opacity: 1; | |
visibility: visible; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50"> | |
<div class="min-h-screen pb-12"> | |
<!-- ============ رأس الصفحة محّسن ============ --> | |
<header class="bg-gradient-to-r from-blue-600 to-indigo-800 text-white py-10 mb-6 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 pulse-animation"></div> | |
<div class="company-name text-2xl font-bold">شركة الريحان</div> | |
</div> | |
</div> | |
<h1 class="text-4xl sm:text-5xl font-bold text-center mb-4 animate-scale">المراجع الذكي</h1> | |
<p class="text-center text-xl text-blue-100 opacity-90">نظام متكامل لمقارنة وتحليل النصوص المترجمة</p> | |
<!-- 4. زر وضع التركيز - جديد --> | |
<div class="flex justify-center mt-4"> | |
<button id="focusModeBtn" class="focus-mode-btn"> | |
<i class="fas fa-compress-alt"></i> | |
<span>وضع التركيز</span> | |
</button> | |
</div> | |
</div> | |
</header> | |
<!-- ============ المحتوى الرئيسي ============ --> | |
<main class="max-w-6xl mx-auto px-4"> | |
<!-- قسم رفع الملفات - محسن --> | |
<div class="bg-white rounded-xl shadow-md p-6 border border-gray-100 hover:shadow-lg transition-all animate-scale mb-6 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="file-type-selector"> | |
<h3 class="text-lg font-bold text-gray-700 mb-3 flex items-center"> | |
<i class="fas fa-cog text-blue-600 ml-2"></i> نوع الملف المُحمّل | |
</h3> | |
<p class="text-sm text-gray-600 mb-3">اختر نوع الملف لتحديد طريقة المعالجة المناسبة</p> | |
<div class="file-type-options"> | |
<div class="file-type-option selected" data-type="normal"> | |
<input type="radio" id="normalFile" name="fileType" value="normal" checked> | |
<label for="normalFile"> | |
<i class="fas fa-file-alt"></i> | |
ملف عادي | |
</label> | |
</div> | |
<div class="file-type-option" data-type="official"> | |
<input type="radio" id="officialFile" name="fileType" value="official"> | |
<label for="officialFile"> | |
<i class="fas fa-certificate"></i> | |
مستند رسمي | |
</label> | |
</div> | |
</div> | |
<div class="mt-3 p-3 bg-blue-50 rounded-lg"> | |
<p class="text-sm text-blue-800" id="fileTypeDescription"> | |
<i class="fas fa-info-circle ml-1"></i> | |
الملف العادي سيتم تحليله مباشرة بدون تصحيح مسبق | |
</p> | |
</div> | |
<!-- 1. قائمة القوالب الرسمية - جديد --> | |
<div id="docTemplateContainer" class="template-selector"> | |
<label for="docTemplate" class="block text-sm font-bold text-gray-700 mb-2">اختر نوع المستند الرسمي:</label> | |
<select id="docTemplate" class="form-select"> | |
<option value="birth">شهادة ميلاد</option> | |
<option value="id">بطاقة شخصية</option> | |
<option value="passport">جواز سفر</option> | |
<option value="driving">رخصة قيادة</option> | |
<option value="other">غير ذلك...</option> | |
</select> | |
<div class="template-info" id="templateInfo"> | |
سيتم تطبيق قواعد التصحيح الخاصة بشهادات الميلاد | |
</div> | |
</div> | |
</div> | |
<!-- حالة التصحيح - جديد --> | |
<div id="correctionStatus" class="correction-status"> | |
<h4> | |
<i class="fas fa-magic"></i> | |
جاري تصحيح النص الرسمي... | |
</h4> | |
<p id="correctionMessage">جاري تحديد الدولة وتطبيق قواعد التصحيح المناسبة...</p> | |
<div class="correction-progress"> | |
<div id="correctionProgressBar" class="correction-progress-bar"></div> | |
</div> | |
</div> | |
<!-- معاينة التصحيح - جديد --> | |
<div id="correctionPreview" class="correction-preview"> | |
<h4> | |
<i class="fas fa-check-circle"></i> | |
معاينة التصحيحات المطبقة | |
</h4> | |
<div id="correctionChanges" class="correction-changes"> | |
<!-- سيتم عرض التغييرات هنا --> | |
</div> | |
<div class="mt-3 flex justify-between items-center"> | |
<span class="text-sm text-green-600"> | |
<i class="fas fa-info-circle ml-1"></i> | |
تم تصحيح الثوابت مع الحفاظ على تنسيق النص الأصلي | |
</span> | |
<button id="proceedWithCorrectedText" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg text-sm"> | |
متابعة بالنص المُصحح | |
</button> | |
</div> | |
</div> | |
<!-- إحصائيات OCR --> | |
<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,.xlsx,.xls" 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> | |
<!-- ملف التارجت --> | |
<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,.xlsx,.xls" 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> | |
</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> | |
<!-- عرض صفحات PDF --> | |
<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> | |
<!-- عرض ملفات Excel --> | |
<div id="excelPreviewCard" class="hidden mt-6"> | |
<h3 class="text-xl font-bold mb-4 text-gray-800">محتوى ملف Excel</h3> | |
<div class="excel-controls" id="sheetSelectorContainer"> | |
<!-- سيتم إضافة أزرار لاختيار الأوراق ديناميكيًا --> | |
</div> | |
<div class="excel-preview" id="excelContent"> | |
<!-- سيتم عرض جدول Excel هنا --> | |
</div> | |
<div class="mt-3 flex space-x-2"> | |
<button id="useExcelAsSourceBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> | |
استخدام المحتوى كنص مصدر | |
</button> | |
<button id="useExcelAsTargetBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> | |
استخدام المحتوى كنص هدف | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- قسم إدخال النصوص يدويًا - محسن --> | |
<div class="bg-white rounded-xl shadow-md p-6 border border-gray-100 hover:shadow-lg transition-all animate-scale mb-6 card-hover"> | |
<h2 class="text-2xl font-bold mb-6 text-gray-800 flex items-center border-b pb-3"> | |
<i class="fas fa-pen-alt text-blue-600 ml-2"></i> إدخال النصوص | |
</h2> | |
<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 bg-blue-50 p-3 rounded-lg"> | |
<input type="checkbox" id="terminologyCheck" class="ml-2 w-5 h-5 text-blue-600 rounded"> | |
<label for="terminologyCheck" class="text-lg text-gray-700">استخدام قاعدة المصطلحات</label> | |
<div class="mr-auto"> | |
<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">ميزة إضافية</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- قسم المصادر الإضافية - محسن --> | |
<div class="bg-white rounded-xl shadow-md p-6 border border-gray-100 hover:shadow-lg transition-all animate-scale mb-6 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,.xlsx,.xls" 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 pulse-animation"> | |
<div class="flex items-center justify-center"> | |
<i class="fas fa-sync-alt ml-2"></i> تحليل النصوص | |
</div> | |
</button> | |
<!-- قسم نتائج التحليل - محسن مع أزرار اختيار طريقة العرض المنفصلة --> | |
<div id="resultSection" class="bg-white rounded-xl shadow-md p-6 border border-gray-100 hover:shadow-lg 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="bg-gray-50 p-4 rounded-xl mb-4"> | |
<h3 class="text-lg font-bold mb-3 text-gray-700 flex items-center"> | |
<i class="fas fa-filter text-blue-500 ml-2"></i> فلترة الأخطاء حسب النوع | |
</h3> | |
<div class="error-filters"> | |
<div class="error-filter filter-all active" data-filter="all"> | |
<i class="fas fa-layer-group"></i> جميع الأخطاء | |
</div> | |
<div class="error-filter filter-numbers" data-filter="number"> | |
<i class="fas fa-hashtag"></i> أخطاء الأرقام | |
</div> | |
<div class="error-filter filter-missing" data-filter="missing"> | |
<i class="fas fa-minus-circle"></i> النصوص المفقودة | |
</div> | |
<div class="error-filter filter-meaning" data-filter="meaning"> | |
<i class="fas fa-exclamation-circle"></i> اختلافات المعنى | |
</div> | |
</div> | |
</div> | |
<!-- أزرار اختيار طريقة العرض - محسنة --> | |
<div class="view-selector mb-4"> | |
<div class="view-btn active" data-view="segmentView"> | |
<i class="fas fa-th-large"></i> العرض المقسم | |
</div> | |
<div class="view-btn" data-view="interactiveView"> | |
<i class="fas fa-exchange-alt"></i> العرض التفاعلي | |
</div> | |
</div> | |
<!-- 2. طريقة العرض المقسمة - محسنة ونشطة افتراضيًا --> | |
<div id="segmentView" class="view-container active"> | |
<div id="segmentedComparisonContainer" class="space-y-4"> | |
<!-- سيتم إنشاء المقاطع هنا بواسطة JavaScript --> | |
</div> | |
<!-- قسم شرح الأخطاء للعرض المقسم - محسن --> | |
<div class="analysis-summary mt-6"> | |
<div class="analysis-summary-header"> | |
<i class="fas fa-clipboard-check"></i> شرح الأخطاء في المقاطع | |
</div> | |
<div class="analysis-summary-body" id="segmentViewExplanation"> | |
<!-- سيتم تعبئته بواسطة JavaScript --> | |
</div> | |
</div> | |
</div> | |
<!-- 3. طريقة العرض التفاعلية - محسنة --> | |
<div id="interactiveView" class="view-container"> | |
<div class="bg-blue-50 rounded-xl p-6 mb-4"> | |
<div class="flex flex-wrap justify-between items-center mb-4"> | |
<h4 class="text-lg font-bold text-blue-800 flex items-center"> | |
<i class="fas fa-exchange-alt ml-2"></i> عرض تفاعلي للاختلافات | |
</h4> | |
<div class="flex space-x-2 mt-2 sm:mt-0"> | |
<button id="prevDiff" class="bg-blue-200 hover:bg-blue-300 text-blue-800 px-3 py-1 rounded-lg disabled:opacity-50 transition-all"> | |
<i class="fas fa-arrow-right ml-1"></i> السابق | |
</button> | |
<span id="diffCounter" class="bg-white px-3 py-1 rounded-lg">0/0</span> | |
<button id="nextDiff" class="bg-blue-200 hover:bg-blue-300 text-blue-800 px-3 py-1 rounded-lg disabled:opacity-50 transition-all"> | |
<i class="fas fa-arrow-left ml-1"></i> التالي | |
</button> | |
</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 class="diff-source-target hidden" id="diffDetailedView"> | |
<div class="diff-panel diff-source"> | |
<div class="diff-panel-header"> | |
<i class="fas fa-file-alt"></i> النص في المصدر | |
</div> | |
<div id="diffSourceText"></div> | |
</div> | |
<div class="diff-panel diff-target"> | |
<div class="diff-panel-header"> | |
<i class="fas fa-file-alt"></i> النص في الهدف | |
</div> | |
<div id="diffTargetText"></div> | |
</div> | |
</div> | |
<!-- إضافة مرجع الاختلاف --> | |
<div class="diff-reference hidden" id="diffReference"> | |
<div class="reference-header"> | |
<i class="fas fa-info-circle"></i> مرجع الاختلاف | |
</div> | |
<div id="diffReferenceText"></div> | |
</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 flex items-center"> | |
<i class="fas fa-chart-pie ml-2"></i> ملخص الاختلافات | |
</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 flex items-center"> | |
<i class="fas fa-lightbulb ml-2"></i> توصيات المعالجة | |
</h5> | |
<div id="diffRecommendations" class="space-y-2 text-green-800"> | |
<!-- سيتم إضافة التوصيات هنا عن طريق JavaScript --> | |
</div> | |
</div> | |
</div> | |
<!-- قسم توضيحي للعرض التفاعلي --> | |
<div class="analysis-summary mt-6"> | |
<div class="analysis-summary-header"> | |
<i class="fas fa-clipboard-check"></i> التفاصيل والتوضيحات | |
</div> | |
<div class="analysis-summary-body" id="interactiveViewExplanation"> | |
<!-- سيتم تعبئته بواسطة JavaScript --> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- مؤشر التحميل غير المتزامن الجديد --> | |
<div id="asyncLoadingIndicator" class="async-loading-indicator"> | |
<div class="async-spinner"></div> | |
<div id="asyncLoadingText" class="font-bold text-gray-700 mt-2">جاري معالجة البيانات...</div> | |
<div id="asyncLoadingProgress" class="text-sm text-gray-500 mt-1">0%</div> | |
</div> | |
<!-- نافذة منبثقة محسنة لشرح الأخطاء --> | |
<div id="popup-overlay" class="popup-overlay"></div> | |
<div id="enhancedErrorPopup" class="enhanced-popup"> | |
<div class="enhanced-popup-header"> | |
<div class="enhanced-popup-title"> | |
<i class="fas fa-exclamation-circle"></i> | |
<span id="enhancedPopupTitle">تفاصيل الخطأ</span> | |
</div> | |
<button class="enhanced-popup-close">×</button> | |
</div> | |
<div id="enhancedPopupContent" class="enhanced-popup-body"> | |
<!-- سيتم إضافة محتوى شرح الخطأ هنا بواسطة JavaScript --> | |
</div> | |
<div class="enhanced-popup-footer"> | |
<button id="closeEnhancedPopup" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg transition-all"> | |
فهمت | |
</button> | |
</div> | |
</div> | |
<!-- قسم المسودة (تقسيمات الفقرات) - محسن ومخفي بشكل افتراضي --> | |
<div id="fullTextDraftSection" class="bg-white rounded-xl shadow-md p-6 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"> | |
<!-- سيتم إنشاء التقسيمات هنا بواسطة JavaScript --> | |
</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 transition-all"> | |
<i class="fas fa-eye ml-2"></i> عرض/إخفاء مسودة التحليل | |
</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 transition-all"> | |
<i class="fas fa-file-excel ml-2"></i> تنزيل التقرير (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 transition-all"> | |
<i class="fas fa-file-word ml-2"></i> تنزيل التقرير (Word) | |
</button> | |
</div> | |
</div> | |
<!-- 5. خريطة الحرارة - جديد --> | |
<div id="heatBar" class="heat-bar"> | |
<!-- نقاط الحرارة ستضاف هنا من خلال JavaScript --> | |
</div> | |
<!-- حقوق الملكية - إضافة جديدة --> | |
<footer class="copyright"> | |
<p>جميع الحقوق محفوظة لشركة فاست برو للبرمجيات والذكاء الاصطناعي © <span id="currentYear"></span></p> | |
<script>document.getElementById('currentYear').textContent = new Date().getFullYear();</script> | |
</footer> | |
</main> | |
</div> | |
<!-- 2. زر الإعدادات ولوحة التحكم - جديد --> | |
<div id="settingsBtn" class="settings-btn"> | |
<i class="fas fa-cog"></i> | |
</div> | |
<div id="settingsPanel" class="settings-panel"> | |
<div class="settings-header"> | |
<i class="fas fa-cog"></i> | |
<span class="font-bold">إعدادات العرض</span> | |
</div> | |
<div class="settings-option"> | |
<span>مسودة التحليل</span> | |
<label class="switch"> | |
<input type="checkbox" id="switchDraft" checked> | |
<span class="slider"></span> | |
</label> | |
</div> | |
<div class="settings-option"> | |
<span>شروحات الأخطاء</span> | |
<label class="switch"> | |
<input type="checkbox" id="switchExplain" checked> | |
<span class="slider"></span> | |
</label> | |
</div> | |
</div> | |
<!-- ================================ | |
جافا سكريبت: الوظائف والمعالجة | |
================================= --> | |
<script> | |
// تعريف متغيرات PDF.js | |
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js'; | |
// إعدادات API - تم تحديثها من النموذج الصغير الناجح | |
const RAPIDAPI_KEY = '32769fb369mshfdf6f5e28e26674p1f3764jsn2a31085a1fc7'; | |
const OCR_API_URL = 'https://ocr43.p.rapidapi.com/v1/results'; | |
const DEEPSEEK_API_KEY = 'sk-15606736ed9e4aea8b7cc11a195d2b01'; | |
const DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions'; | |
// متغيرات عامة للتحليل | |
let fullAnalysisText = ''; // متغير لتخزين النص الكامل للتحليل | |
let analysisSegments = []; // متغير لتخزين مقاطع التحليل المقسمة | |
let allDifferences = []; // متغير لتخزين جميع الاختلافات للعرض التفاعلي | |
let currentDiffIndex = -1; // مؤشر للاختلاف الحالي | |
// متغيرات OCR | |
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; | |
// متغيرات جديدة للتحليل المتقدم | |
let sentenceErrors = []; // تخزين الأخطاء على مستوى الجملة | |
let errorExplanations = {}; // تخزين شروحات الأخطاء | |
// متغير للفلتر الحالي | |
let currentErrorFilter = 'all'; | |
// متغيرات Excel | |
let excelWorkbook = null; | |
let currentSheetName = ''; | |
let excelData = null; | |
// متغيرات جديدة لتصحيح المستندات الرسمية | |
let selectedFileType = 'normal'; // افتراضي: ملف عادي | |
let detectedCountry = null; // الدولة المكتشفة | |
let correctionApplied = false; // تم تطبيق التصحيح أم لا | |
let originalTextBeforeCorrection = ''; // النص الأصلي قبل التصحيح | |
let correctedText = ''; // النص بعد التصحيح | |
let correctionChanges = []; // التغييرات المطبقة | |
// متغيرات جديدة للإعدادات والأوضاع | |
let showDraft = true; // متغير لعرض/إخفاء مسودة التحليل | |
let showExplanations = true; // متغير لعرض/إخفاء شروحات الأخطاء | |
let focusModeActive = false; // متغير لتفعيل/إلغاء وضع التركيز | |
/* ===================================== | |
تهيئة الصفحة وتحميل الإعدادات المحفوظة | |
===================================== */ | |
document.addEventListener('DOMContentLoaded', function() { | |
// استرداد عداد OCR وتاريخ آخر معالجة من localStorage | |
const savedCount = localStorage.getItem('ocrPagesCount'); | |
const lastDate = localStorage.getItem('lastOcrDate'); | |
// استرداد إعدادات العرض من localStorage | |
const savedShowDraft = localStorage.getItem('showDraft'); | |
const savedShowExplain = localStorage.getItem('showExplain'); | |
if (savedCount) { | |
ocrPagesCount = parseInt(savedCount); | |
document.getElementById('ocrCounter').textContent = ocrPagesCount; | |
} | |
if (lastDate) { | |
document.getElementById('lastOcrDate').textContent = lastDate; | |
} | |
// تحميل إعدادات العرض | |
if (savedShowDraft !== null) { | |
showDraft = savedShowDraft === '1'; | |
document.getElementById('switchDraft').checked = showDraft; | |
} | |
if (savedShowExplain !== null) { | |
showExplanations = savedShowExplain === '1'; | |
document.getElementById('switchExplain').checked = showExplanations; | |
} | |
// تهيئة النافذة المنبثقة المحسنة | |
initEnhancedPopup(); | |
// تهيئة اختيار نوع الملف | |
initFileTypeSelector(); | |
// تهيئة زر الإعدادات ولوحة التحكم | |
initSettingsPanel(); | |
// تهيئة وضع التركيز | |
initFocusMode(); | |
// إزالة العرض الكلاسيكي وتعيين العرض المقسم كافتراضي | |
setupViewModes(); | |
// تهيئة خريطة الحرارة | |
initHeatMap(); | |
// إضافة أحداث لأزرار عرض PDF | |
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'); | |
}); | |
// إضافة أحداث لأزرار Excel | |
document.getElementById('useExcelAsSourceBtn')?.addEventListener('click', function() { | |
useExcelContent('source'); | |
}); | |
document.getElementById('useExcelAsTargetBtn')?.addEventListener('click', function() { | |
useExcelContent('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); | |
// إضافة حدث للمتابعة بالنص المُصحح | |
document.getElementById('proceedWithCorrectedText')?.addEventListener('click', function() { | |
// إخفاء معاينة التصحيح | |
document.getElementById('correctionPreview').classList.remove('active'); | |
// استخدام النص المُصحح في المكان المناسب | |
if (currentProcessingMode === 'source') { | |
document.getElementById('sourceText').value = correctedText; | |
addError('تم تطبيق التصحيحات على النص المصدر', 'info'); | |
} else if (currentProcessingMode === 'target') { | |
document.getElementById('targetText').value = correctedText; | |
addError('تم تطبيق التصحيحات على النص الهدف', 'info'); | |
} else if (currentProcessingMode === 'extra') { | |
document.getElementById('sourceExtraText').value = correctedText; | |
addError('تم تطبيق التصحيحات على المصدر الإضافي', 'info'); | |
} | |
}); | |
// إضافة حدث للقالب المحدد | |
document.getElementById('docTemplate')?.addEventListener('change', updateTemplateInfo); | |
// إضافة أحداث التلقائية لمعالجة الملفات | |
document.getElementById('sourceFile').addEventListener('change', function(event) { | |
const file = event.target.files[0]; | |
if (file) { | |
processFile(file, 'source'); | |
} | |
}); | |
document.getElementById('targetFile').addEventListener('change', function(event) { | |
const file = event.target.files[0]; | |
if (file) { | |
processFile(file, 'target'); | |
} | |
}); | |
document.getElementById('sourceExtraFile').addEventListener('change', function(event) { | |
const file = event.target.files[0]; | |
if (file) { | |
processFile(file, 'extra'); | |
} | |
}); | |
// إضافة أحداث لأزرار طرق العرض المنفصلة | |
document.querySelectorAll('.view-btn').forEach(btn => { | |
btn.addEventListener('click', function() { | |
// إزالة الفئة active من جميع الأزرار والحاويات | |
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); | |
document.querySelectorAll('.view-container').forEach(c => c.classList.remove('active')); | |
// تفعيل الزر والحاوية المحددين | |
this.classList.add('active'); | |
const viewId = this.getAttribute('data-view'); | |
document.getElementById(viewId).classList.add('active'); | |
}); | |
}); | |
// إضافة أحداث لأزرار فلتر الأخطاء | |
document.querySelectorAll('.error-filter').forEach(filter => { | |
filter.addEventListener('click', function() { | |
// إزالة الفئة active من جميع الأزرار | |
document.querySelectorAll('.error-filter').forEach(f => f.classList.remove('active')); | |
// تفعيل الزر المحدد | |
this.classList.add('active'); | |
// تحديث الفلتر الحالي | |
currentErrorFilter = this.getAttribute('data-filter'); | |
// تطبيق الفلتر على جميع طرق العرض | |
applyErrorFilter(currentErrorFilter); | |
// تحديث خريطة الحرارة بعد تطبيق الفلتر | |
updateHeatMap(); | |
}); | |
}); | |
// إضافة حدث للتبديل بين عرض وإخفاء المسودة | |
document.getElementById('toggleDraftBtn').addEventListener('click', function() { | |
const draftSection = document.getElementById('fullTextDraftSection'); | |
draftSection.classList.toggle('hidden'); | |
// تغيير نص الزر | |
this.innerHTML = draftSection.classList.contains('hidden') ? | |
'<i class="fas fa-eye ml-2"></i> عرض مسودة التحليل' : | |
'<i class="fas fa-eye-slash ml-2"></i> إخفاء مسودة التحليل'; | |
}); | |
// إضافة حدث لتنزيل التقرير بصيغة Excel | |
document.getElementById('downloadExcelBtn').addEventListener('click', function() { | |
// إنشاء تقرير Excel بناء على نتائج التحليل | |
if (!analysisSegments || analysisSegments.length === 0) { | |
alert('لا توجد نتائج تحليل للتنزيل'); | |
return; | |
} | |
try { | |
// إنشاء مصفوفة البيانات للتقرير | |
const reportData = []; | |
// إضافة رأس الجدول | |
reportData.push(['رقم المقطع', 'النص المصدر', 'النص الهدف', 'الأخطاء', 'التحليل']); | |
// إضافة بيانات التحليل | |
analysisSegments.forEach((segment, index) => { | |
const errors = []; | |
if (segment.errors.numbers > 0) errors.push(`اختلافات أرقام: ${segment.errors.numbers}`); | |
if (segment.errors.missing > 0) errors.push(`نصوص مفقودة: ${segment.errors.missing}`); | |
if (segment.errors.meaning > 0) errors.push(`اختلافات معنى: ${segment.errors.meaning}`); | |
reportData.push([ | |
index + 1, | |
segment.source, | |
segment.target, | |
errors.join('\n'), | |
segment.analysis | |
]); | |
}); | |
// إنشاء ورقة عمل | |
const ws = XLSX.utils.aoa_to_sheet(reportData); | |
// إنشاء المصنف | |
const wb = XLSX.utils.book_new(); | |
XLSX.utils.book_append_sheet(wb, ws, "تقرير التحليل"); | |
// تنزيل الملف | |
XLSX.writeFile(wb, "تقرير_تحليل_النصوص.xlsx"); | |
} catch (error) { | |
console.error("خطأ في إنشاء ملف Excel:", error); | |
alert("حدث خطأ أثناء إنشاء التقرير"); | |
} | |
}); | |
// إضافة حدث لتنزيل التقرير بصيغة Word | |
document.getElementById('downloadWordBtn').addEventListener('click', function() { | |
// سيتم تنفيذ تنزيل التقرير بصيغة Word | |
downloadWordReport(); | |
}); | |
}); | |
/* ===================================== | |
1. تهيئة وتحديث معلومات القالب الرسمي - جديد | |
===================================== */ | |
function updateTemplateInfo() { | |
const templateSelect = document.getElementById('docTemplate'); | |
const templateInfo = document.getElementById('templateInfo'); | |
if (!templateSelect || !templateInfo) return; | |
const selectedTemplate = templateSelect.value; | |
let infoText = ''; | |
switch (selectedTemplate) { | |
case 'birth': | |
infoText = 'سيتم تطبيق قواعد التصحيح الخاصة بشهادات الميلاد'; | |
break; | |
case 'id': | |
infoText = 'سيتم تطبيق قواعد التصحيح الخاصة ببطاقات الهوية الشخصية'; | |
break; | |
case 'passport': | |
infoText = 'سيتم تطبيق قواعد التصحيح الخاصة بجوازات السفر'; | |
break; | |
case 'driving': | |
infoText = 'سيتم تطبيق قواعد التصحيح الخاصة برخص القيادة'; | |
break; | |
case 'other': | |
infoText = 'سيتم تطبيق قواعد التصحيح العامة للوثائق الرسمية'; | |
break; | |
} | |
templateInfo.textContent = infoText; | |
} | |
/* ===================================== | |
2. تهيئة لوحة الإعدادات - جديد | |
===================================== */ | |
function initSettingsPanel() { | |
const settingsBtn = document.getElementById('settingsBtn'); | |
const settingsPanel = document.getElementById('settingsPanel'); | |
const switchDraft = document.getElementById('switchDraft'); | |
const switchExplain = document.getElementById('switchExplain'); | |
// عرض/إخفاء لوحة الإعدادات عند النقر على الزر | |
settingsBtn.addEventListener('click', function() { | |
settingsPanel.classList.toggle('active'); | |
}); | |
// إضافة الأحداث لمفاتيح التبديل | |
switchDraft.addEventListener('change', function() { | |
showDraft = this.checked; | |
localStorage.setItem('showDraft', showDraft ? '1' : '0'); | |
// تطبيق التغييرات مباشرة | |
toggleDraftDisplay(); | |
}); | |
switchExplain.addEventListener('change', function() { | |
showExplanations = this.checked; | |
localStorage.setItem('showExplain', showExplanations ? '1' : '0'); | |
// تطبيق التغييرات مباشرة | |
toggleExplanationsDisplay(); | |
}); | |
// إخفاء لوحة الإعدادات عند النقر خارجها | |
document.addEventListener('click', function(event) { | |
if (!settingsBtn.contains(event.target) && !settingsPanel.contains(event.target)) { | |
settingsPanel.classList.remove('active'); | |
} | |
}); | |
} | |
/* ===================================== | |
2. وظائف عرض/إخفاء المسودة والشروحات - جديد | |
===================================== */ | |
function toggleDraftDisplay() { | |
const draftSection = document.getElementById('fullTextDraftSection'); | |
if (!draftSection) return; | |
if (showDraft) { | |
// استعادة العرض إذا كانت مرئية سابقًا | |
if (!draftSection.classList.contains('hidden-by-settings')) { | |
draftSection.classList.remove('hidden'); | |
} | |
} else { | |
// حفظ الحالة الأصلية ثم إخفاء | |
if (!draftSection.classList.contains('hidden')) { | |
draftSection.classList.add('hidden-by-settings'); | |
} | |
draftSection.classList.add('hidden'); | |
} | |
// تحديث زر التبديل أيضًا | |
const toggleBtn = document.getElementById('toggleDraftBtn'); | |
if (toggleBtn) { | |
if (!showDraft) { | |
toggleBtn.classList.add('hidden'); | |
} else { | |
toggleBtn.classList.remove('hidden'); | |
} | |
} | |
} | |
function toggleExplanationsDisplay() { | |
// إخفاء/إظهار أقسام شرح الأخطاء | |
const explanationSections = document.querySelectorAll('.analysis-summary'); | |
explanationSections.forEach(section => { | |
if (showExplanations) { | |
section.classList.remove('hidden'); | |
} else { | |
section.classList.add('hidden'); | |
} | |
}); | |
// إخفاء/إظهار التوضيحات في النصوص أيضًا | |
const explanationElements = document.querySelectorAll('.segment-notes'); | |
explanationElements.forEach(element => { | |
if (showExplanations) { | |
element.classList.remove('hidden'); | |
} else { | |
element.classList.add('hidden'); | |
} | |
}); | |
} | |
/* ===================================== | |
3. دالة إعادة تحليل فقرة محددة - جديد | |
===================================== */ | |
async function reanalyzeSegment(segmentIndex) { | |
if (!analysisSegments || !analysisSegments[segmentIndex]) { | |
console.error('لا يمكن إعادة تحليل المقطع: المقطع غير موجود'); | |
return; | |
} | |
try { | |
// تحويل زر إعادة التحليل إلى حالة التحميل | |
const reanalyzeBtn = document.querySelector(`.segment-comparison[data-segment-index="${segmentIndex}"] .reanalyze-btn`); | |
if (reanalyzeBtn) { | |
reanalyzeBtn.classList.add('spin'); | |
} | |
// استخراج النصوص المصدر والهدف للمقطع المحدد | |
const sourceText = analysisSegments[segmentIndex].source; | |
const targetText = analysisSegments[segmentIndex].target; | |
// إظهار مؤشر التحميل | |
showLoadingIndicator(`جاري إعادة تحليل المقطع ${segmentIndex + 1}...`, '0%'); | |
// استدعاء التحليل للمقطع المحدد فقط | |
updateLoadingProgress(`جاري تحليل المقطع ${segmentIndex + 1}...`, '50%'); | |
const analysisResult = await analyzeAlignedPair(sourceText, targetText, segmentIndex + 1); | |
// تحديث نتائج التحليل في واجهة المستخدم | |
updateSegmentAnalysis(segmentIndex, analysisResult); | |
// تحديث المقطع في العرض المقسم | |
updateSegmentDisplay(segmentIndex, analysisResult); | |
// تحديث متغير تخزين التحليل | |
analysisSegments[segmentIndex].analysis = analysisResult.analysis; | |
analysisSegments[segmentIndex].errors = analysisResult.errors; | |
// تحديث العرض التفاعلي وملخصات الأخطاء | |
updateAnalysisSummary(); | |
// تحديث خريطة الحرارة | |
updateHeatMap(); | |
// إخفاء مؤشر التحميل | |
updateLoadingProgress('تم إعادة التحليل بنجاح!', '100%'); | |
setTimeout(() => { | |
hideLoadingIndicator(); | |
// إعادة زر التحليل إلى حالته الطبيعية | |
if (reanalyzeBtn) { | |
reanalyzeBtn.classList.remove('spin'); | |
} | |
// إظهار رسالة نجاح | |
addError(`تم إعادة تحليل المقطع ${segmentIndex + 1} بنجاح`, 'info'); | |
}, 1000); | |
} catch (error) { | |
console.error('خطأ في إعادة تحليل المقطع:', error); | |
// إعادة زر التحليل إلى حالته الطبيعية | |
const reanalyzeBtn = document.querySelector(`.segment-comparison[data-segment-index="${segmentIndex}"] .reanalyze-btn`); | |
if (reanalyzeBtn) { | |
reanalyzeBtn.classList.remove('spin'); | |
} | |
hideLoadingIndicator(); | |
addError(`خطأ في إعادة تحليل المقطع: ${error.message}`, 'error'); | |
} | |
} | |
// تحديث عرض المقطع بعد إعادة التحليل | |
function updateSegmentDisplay(segmentIndex, analysisResult) { | |
const segmentElement = document.querySelector(`.segment-comparison[data-segment-index="${segmentIndex}"]`); | |
if (!segmentElement) return; | |
// تحديث علامات الأخطاء | |
const tagsContainer = segmentElement.querySelector('.segment-header > div'); | |
if (tagsContainer) { | |
let tagHTML = ''; | |
if (analysisResult.errors.numbers > 0) { | |
tagHTML += '<span class="segment-tag tag-error">أخطاء أرقام</span>'; | |
} | |
if (analysisResult.errors.missing > 0) { | |
tagHTML += '<span class="segment-tag tag-warning">نصوص مفقودة</span>'; | |
} | |
if (analysisResult.errors.meaning > 0) { | |
tagHTML += '<span class="segment-tag tag-info">اختلاف معنى</span>'; | |
} | |
if (analysisResult.errors.numbers === 0 && | |
analysisResult.errors.missing === 0 && | |
analysisResult.errors.meaning === 0) { | |
tagHTML = '<span class="segment-tag tag-success">مطابق</span>'; | |
} | |
tagsContainer.innerHTML = tagHTML; | |
} | |
// تحديث محتوى التحليل | |
const notesContainer = segmentElement.querySelector('.segment-notes'); | |
if (notesContainer) { | |
notesContainer.innerHTML = formatAnalysisText(analysisResult.analysis); | |
} | |
// تحديث المحتوى المصدر والهدف مع التمييز الجديد | |
const sourceContainer = segmentElement.querySelector('.segment-source'); | |
const targetContainer = segmentElement.querySelector('.segment-target'); | |
if (sourceContainer && targetContainer) { | |
const sourceHighlighted = applyHighlights(analysisResult.sourceText, analysisResult.targetText, analysisResult.analysis); | |
const targetHighlighted = applyHighlights(analysisResult.targetText, analysisResult.sourceText, analysisResult.analysis); | |
sourceContainer.innerHTML = sourceHighlighted; | |
targetContainer.innerHTML = targetHighlighted; | |
} | |
// إضافة مستمعي الأحداث للتحديدات الجديدة | |
setTimeout(() => { | |
segmentElement.querySelectorAll('.sentence-with-error').forEach(element => { | |
element.addEventListener('click', function() { | |
handleErrorClick(this); | |
}); | |
}); | |
}, 100); | |
} | |
/* ===================================== | |
3. معالجة النقر على عناصر الخطأ - مساعد لإعادة التحليل | |
===================================== */ | |
function handleErrorClick(element) { | |
// الحصول على النص الكامل للجملة | |
const sentenceText = element.textContent; | |
// تحديد نوع الخطأ من خلال الفئات داخل الجملة | |
let errorType = 'general'; | |
let errorSpecificText = ''; | |
// البحث عن الخطأ المحدد داخل الجملة | |
const numberHighlight = element.querySelector('.highlight-number'); | |
const missingHighlight = element.querySelector('.highlight-missing'); | |
const meaningHighlight = element.querySelector('.highlight-meaning'); | |
if (numberHighlight) { | |
errorType = 'number'; | |
errorSpecificText = numberHighlight.textContent; | |
} else if (missingHighlight) { | |
errorType = 'missing'; | |
errorSpecificText = missingHighlight.textContent; | |
} else if (meaningHighlight) { | |
errorType = 'meaning'; | |
errorSpecificText = meaningHighlight.textContent; | |
} | |
const sentenceNumber = element.getAttribute('data-sentence-number'); | |
// إعداد أمثلة حسب نوع الخطأ | |
let examples = null; | |
if (errorType === 'number') { | |
examples = { | |
incorrect: `المادة <span class="highlight-number">٥</span> من القانون، تُطبق الغرامة المقدرة <span class="highlight-number">٤٥٠</span> دينارًا.`, | |
correct: `المادة <span class="corrected-text">٥</span> من القانون، تُطبق الغرامة المقدرة <span class="corrected-text">٤٥٠</span> دينارًا.` | |
}; | |
} else if (errorType === 'missing') { | |
examples = { | |
incorrect: `اتفق الطرفان على أن يتم تسليم البضائع <span class="highlight-missing">خلال 30 يومًا من توقيع العقد</span>.`, | |
correct: `اتفق الطرفان على أن يتم تسليم البضائع <span class="corrected-text">خلال 30 يومًا من توقيع العقد</span>.` | |
}; | |
} else if (errorType === 'meaning') { | |
examples = { | |
incorrect: `أقرت المحكمة <span class="highlight-meaning">بإدانة</span> المتهم.`, | |
correct: `أقرت المحكمة <span class="corrected-text">ببراءة</span> المتهم.` | |
}; | |
} | |
// إعداد شرح أكثر ودية مع أمثلة | |
let explanation = ''; | |
if (errorType === 'number') { | |
explanation = `يبدو أن هناك اختلافًا في الأرقام في هذه الجملة. الرقم "${errorSpecificText}" في النص المصدر لا يتطابق مع النص الهدف. قد يكون هناك خطأ في نقل الرقم أو تحويله بين أنظمة الأرقام المختلفة (العربية، الإنجليزية، الهندية).`; | |
} else if (errorType === 'missing') { | |
explanation = `هناك نص مفقود في الترجمة! النص "${errorSpecificText}" موجود في المصدر ولكن لم يتم ترجمته أو تضمينه في النص الهدف. هذا يؤدي إلى فقدان جزء من المعنى الأصلي.`; | |
} else if (errorType === 'meaning') { | |
explanation = `هناك اختلاف مهم في المعنى! النص "${errorSpecificText}" تمت ترجمته بطريقة تغير المعنى الأصلي. قد يكون هناك خطأ في فهم السياق أو اختيار المصطلحات المناسبة.`; | |
} else { | |
explanation = `هناك خطأ في هذه الجملة: "${sentenceText}"<br><br>يرجى مراجعة الترجمة للتأكد من دقة المعنى والتطابق بين النصين.`; | |
} | |
// استخدام النافذة المنبثقة المحسنة | |
showEnhancedPopup(errorType, sentenceText, explanation, examples); | |
} | |
/* ===================================== | |
4. تهيئة وضع التركيز - جديد | |
===================================== */ | |
function initFocusMode() { | |
const focusModeBtn = document.getElementById('focusModeBtn'); | |
if (!focusModeBtn) return; | |
focusModeBtn.addEventListener('click', function() { | |
focusModeActive = !focusModeActive; | |
if (focusModeActive) { | |
document.body.classList.add('focus-on'); | |
focusModeBtn.classList.add('active'); | |
} else { | |
document.body.classList.remove('focus-on'); | |
focusModeBtn.classList.remove('active'); | |
} | |
}); | |
} | |
/* ===================================== | |
5. تهيئة وتحديث خريطة الحرارة - جديد | |
===================================== */ | |
function initHeatMap() { | |
// تهيئة حاوية خريطة الحرارة | |
const heatBar = document.getElementById('heatBar'); | |
if (!heatBar) return; | |
// إضافة معلومات توضيحية عند تحويم الماوس | |
heatBar.addEventListener('mouseover', function() { | |
const tooltip = document.createElement('div'); | |
tooltip.className = 'absolute left-12 bg-black bg-opacity-80 text-white p-2 rounded text-xs'; | |
tooltip.style.top = '50%'; | |
tooltip.style.transform = 'translateY(-50%)'; | |
tooltip.innerHTML = 'خريطة حرارة الأخطاء'; | |
// حذف أي تلميحات سابقة | |
const oldTooltip = heatBar.querySelector('.absolute'); | |
if (oldTooltip) { | |
oldTooltip.remove(); | |
} | |
heatBar.appendChild(tooltip); | |
}); | |
heatBar.addEventListener('mouseout', function() { | |
const tooltip = heatBar.querySelector('.absolute'); | |
if (tooltip) { | |
tooltip.remove(); | |
} | |
}); | |
} | |
// تحديث خريطة الحرارة بعد التحليل أو تغيير الفلتر | |
function updateHeatMap() { | |
const heatBar = document.getElementById('heatBar'); | |
if (!heatBar) return; | |
// مسح النقاط الحالية | |
while (heatBar.firstChild) { | |
heatBar.removeChild(heatBar.firstChild); | |
} | |
// الحصول على الطول الكلي للنص | |
let totalTextLength = 0; | |
analysisSegments.forEach(segment => { | |
totalTextLength += segment.source.length; | |
}); | |
if (totalTextLength === 0) return; | |
// معالجة كل مقطع | |
let currentPosition = 0; | |
analysisSegments.forEach((segment, segmentIndex) => { | |
const segmentLength = segment.source.length; | |
const segmentStart = currentPosition / totalTextLength; | |
currentPosition += segmentLength; | |
// إضافة نقاط للأخطاء العددية | |
if (segment.errors.numbers > 0 && (currentErrorFilter === 'all' || currentErrorFilter === 'number')) { | |
for (let i = 0; i < segment.errors.numbers; i++) { | |
const position = (segmentStart + (i * 0.01)) * 100; | |
addHeatDot(position, 'number-error', `المقطع ${segmentIndex + 1} - خطأ رقمي`); | |
} | |
} | |
// إضافة نقاط للنصوص المفقودة | |
if (segment.errors.missing > 0 && (currentErrorFilter === 'all' || currentErrorFilter === 'missing')) { | |
for (let i = 0; i < segment.errors.missing; i++) { | |
const position = (segmentStart + (i * 0.01) + 0.003) * 100; | |
addHeatDot(position, 'missing-error', `المقطع ${segmentIndex + 1} - نص مفقود`); | |
} | |
} | |
// إضافة نقاط لأخطاء المعنى | |
if (segment.errors.meaning > 0 && (currentErrorFilter === 'all' || currentErrorFilter === 'meaning')) { | |
for (let i = 0; i < segment.errors.meaning; i++) { | |
const position = (segmentStart + (i * 0.01) + 0.006) * 100; | |
addHeatDot(position, 'meaning-error', `المقطع ${segmentIndex + 1} - خطأ معنى`); | |
} | |
} | |
}); | |
} | |
// إضافة نقطة إلى خريطة الحرارة | |
function addHeatDot(positionPercent, errorClass, tooltipText) { | |
const heatBar = document.getElementById('heatBar'); | |
if (!heatBar) return; | |
const dot = document.createElement('div'); | |
dot.className = `heat-dot ${errorClass}`; | |
dot.style.top = `${positionPercent}%`; | |
dot.setAttribute('data-tooltip', tooltipText); | |
// إضافة حدث النقر للانتقال إلى المقطع المناسب | |
dot.addEventListener('click', function() { | |
// استخراج رقم المقطع من النص التلميحي | |
const segmentMatch = tooltipText.match(/المقطع (\d+)/); | |
if (segmentMatch && segmentMatch[1]) { | |
const segmentIndex = parseInt(segmentMatch[1]) - 1; | |
scrollToSegment(segmentIndex); | |
} | |
}); | |
heatBar.appendChild(dot); | |
} | |
// التمرير إلى المقطع المحدد | |
function scrollToSegment(segmentIndex) { | |
const segmentElement = document.querySelector(`.segment-comparison[data-segment-index="${segmentIndex}"]`); | |
if (segmentElement) { | |
// التمرير إلى المقطع بتأثير ناعم | |
segmentElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
// إضافة تأثير بصري لتمييز المقطع | |
segmentElement.classList.add('animate-pulse'); | |
setTimeout(() => { | |
segmentElement.classList.remove('animate-pulse'); | |
}, 2000); | |
} | |
} | |
/* ===================================== | |
إعداد طرق العرض وإزالة العرض الكلاسيكي | |
===================================== */ | |
function setupViewModes() { | |
// إزالة العرض الكلاسيكي تمامًا | |
const classicViewBtn = document.querySelector('.view-btn[data-view="classicView"]'); | |
const classicViewContainer = document.getElementById('classicView'); | |
// إزالة زر العرض الكلاسيكي | |
if (classicViewBtn) { | |
classicViewBtn.remove(); | |
} | |
// إزالة حاوية العرض الكلاسيكي | |
if (classicViewContainer) { | |
classicViewContainer.remove(); | |
} | |
// تفعيل العرض المقسم كافتراضي | |
const segmentViewBtn = document.querySelector('.view-btn[data-view="segmentView"]'); | |
if (segmentViewBtn) { | |
segmentViewBtn.classList.add('active'); | |
} | |
const segmentViewContainer = document.getElementById('segmentView'); | |
if (segmentViewContainer) { | |
segmentViewContainer.classList.add('active'); | |
} | |
} | |
/* ===================================== | |
تهيئة اختيار نوع الملف - محسن | |
===================================== */ | |
function initFileTypeSelector() { | |
const fileTypeOptions = document.querySelectorAll('.file-type-option'); | |
const descriptionElement = document.getElementById('fileTypeDescription'); | |
const docTemplateContainer = document.getElementById('docTemplateContainer'); | |
// إضافة مستمعي الأحداث لخيارات نوع الملف | |
fileTypeOptions.forEach(option => { | |
option.addEventListener('click', function() { | |
// إزالة التحديد من جميع الخيارات | |
fileTypeOptions.forEach(opt => opt.classList.remove('selected')); | |
// تحديد الخيار المختار | |
this.classList.add('selected'); | |
// تحديث نوع الملف المختار | |
const radioInput = this.querySelector('input[type="radio"]'); | |
radioInput.checked = true; | |
selectedFileType = radioInput.value; | |
// تحديث الوصف وإظهار/إخفاء قائمة القوالب | |
updateFileTypeDescription(); | |
// إظهار/إخفاء قائمة القوالب الرسمية | |
if (selectedFileType === 'official') { | |
docTemplateContainer.classList.add('active'); | |
updateTemplateInfo(); | |
} else { | |
docTemplateContainer.classList.remove('active'); | |
} | |
}); | |
// إضافة حدث للراديو بوتن نفسه | |
const radioInput = option.querySelector('input[type="radio"]'); | |
radioInput.addEventListener('change', function() { | |
if (this.checked) { | |
fileTypeOptions.forEach(opt => opt.classList.remove('selected')); | |
option.classList.add('selected'); | |
selectedFileType = this.value; | |
updateFileTypeDescription(); | |
// إظهار/إخفاء قائمة القوالب الرسمية | |
if (selectedFileType === 'official') { | |
docTemplateContainer.classList.add('active'); | |
updateTemplateInfo(); | |
} else { | |
docTemplateContainer.classList.remove('active'); | |
} | |
} | |
}); | |
}); | |
// تحديث الوصف الأولي | |
updateFileTypeDescription(); | |
} | |
/* ===================================== | |
تحديث وصف نوع الملف - محسن | |
===================================== */ | |
function updateFileTypeDescription() { | |
const descriptionElement = document.getElementById('fileTypeDescription'); | |
if (selectedFileType === 'normal') { | |
descriptionElement.innerHTML = ` | |
<i class="fas fa-info-circle ml-1"></i> | |
الملف العادي سيتم تحليله مباشرة بدون تصحيح مسبق | |
`; | |
} else if (selectedFileType === 'official') { | |
descriptionElement.innerHTML = ` | |
<i class="fas fa-magic ml-1"></i> | |
المستند الرسمي سيتم تصحيح الثوابت فيه قبل التحليل (مثل تصحيح "الأثم" إلى "الاسم") | |
`; | |
} | |
} | |
/* ===================================== | |
تهيئة النافذة المنبثقة المحسنة | |
===================================== */ | |
function initEnhancedPopup() { | |
const popup = document.getElementById('enhancedErrorPopup'); | |
const overlay = document.getElementById('popup-overlay'); | |
const closeBtn = document.querySelector('.enhanced-popup-close'); | |
const confirmBtn = document.getElementById('closeEnhancedPopup'); | |
// إضافة حدث لزر الإغلاق | |
closeBtn.addEventListener('click', closeEnhancedPopup); | |
confirmBtn.addEventListener('click', closeEnhancedPopup); | |
// إضافة حدث للنقر على الخلفية | |
overlay.addEventListener('click', closeEnhancedPopup); | |
// إضافة مستمع للنقر على مفتاح Escape | |
document.addEventListener('keydown', function(event) { | |
if (event.key === 'Escape') { | |
closeEnhancedPopup(); | |
} | |
}); | |
} | |
/* ===================================== | |
إغلاق النافذة المنبثقة المحسنة | |
===================================== */ | |
function closeEnhancedPopup() { | |
const popup = document.getElementById('enhancedErrorPopup'); | |
const overlay = document.getElementById('popup-overlay'); | |
popup.classList.remove('show'); | |
overlay.classList.remove('show'); | |
} | |
/* ===================================== | |
عرض النافذة المنبثقة المحسنة | |
===================================== */ | |
function showEnhancedPopup(errorType, errorText, explanation, examples = null) { | |
const popup = document.getElementById('enhancedErrorPopup'); | |
const overlay = document.getElementById('popup-overlay'); | |
const title = document.getElementById('enhancedPopupTitle'); | |
const content = document.getElementById('enhancedPopupContent'); | |
// تعيين العنوان حسب نوع الخطأ | |
let typeIcon = ''; | |
let typeClass = ''; | |
let typeTitle = ''; | |
if (errorType === 'number') { | |
typeTitle = 'خطأ في الأرقام'; | |
typeIcon = '<i class="fas fa-hashtag text-yellow-500"></i>'; | |
typeClass = 'border-yellow-400'; | |
} else if (errorType === 'missing') { | |
typeTitle = 'نص مفقود'; | |
typeIcon = '<i class="fas fa-minus-circle text-blue-500"></i>'; | |
typeClass = 'border-blue-400'; | |
} else if (errorType === 'meaning') { | |
typeTitle = 'اختلاف في المعنى'; | |
typeIcon = '<i class="fas fa-exclamation-circle text-red-500"></i>'; | |
typeClass = 'border-red-400'; | |
} else { | |
typeTitle = 'تفاصيل الخطأ'; | |
typeIcon = '<i class="fas fa-info-circle text-blue-500"></i>'; | |
typeClass = 'border-blue-400'; | |
} | |
title.innerHTML = `${typeIcon} ${typeTitle}`; | |
// إعداد المحتوى المحسن | |
let contentHTML = ` | |
<div class="mb-4"> | |
<h3 class="text-xl font-bold mb-3 text-gray-800">النص المحدد:</h3> | |
<div class="p-4 rounded-lg bg-gray-50 border-r-4 ${typeClass} text-lg">${errorText}</div> | |
</div> | |
<div class="mb-4"> | |
<h3 class="text-xl font-bold mb-3 text-gray-800">تفسير الخطأ:</h3> | |
<div class="p-4 rounded-lg bg-blue-50 border border-blue-200 text-lg leading-relaxed">${explanation}</div> | |
</div> | |
`; | |
// إضافة أمثلة إذا كانت متوفرة | |
if (examples) { | |
contentHTML += ` | |
<div class="mb-4"> | |
<h3 class="text-xl font-bold mb-3 text-gray-800">أمثلة توضيحية:</h3> | |
<div class="space-y-4"> | |
<div> | |
<div class="font-bold flex items-center text-red-700 mb-2"> | |
<i class="fas fa-times-circle ml-2"></i> مثال خاطئ: | |
</div> | |
<div class="error-example">${examples.incorrect}</div> | |
</div> | |
<div> | |
<div class="font-bold flex items-center text-green-700 mb-2"> | |
<i class="fas fa-check-circle ml-2"></i> مثال صحيح: | |
</div> | |
<div class="correct-example">${examples.correct}</div> | |
</div> | |
</div> | |
</div> | |
`; | |
} | |
// إضافة توضيح مرئي (صورة) حسب نوع الخطأ | |
let illustrationUrl = ''; | |
if (errorType === 'number') { | |
illustrationUrl = 'https://i.ibb.co/Rvfwhdk/number-error.png'; | |
} else if (errorType === 'missing') { | |
illustrationUrl = 'https://i.ibb.co/wYRgxWV/missing-text.png'; | |
} else if (errorType === 'meaning') { | |
illustrationUrl = 'https://i.ibb.co/MPDc1X4/meaning-error.png'; | |
} | |
if (illustrationUrl) { | |
contentHTML += ` | |
<div class="mb-4"> | |
<h3 class="text-xl font-bold mb-3 text-gray-800">توضيح مرئي:</h3> | |
<img src="${illustrationUrl}" alt="توضيح للخطأ" class="error-illustration"> | |
</div> | |
`; | |
} | |
// إضافة نصائح للتصحيح | |
let tips = ''; | |
if (errorType === 'number') { | |
tips = ` | |
<li>تأكد من تطابق الأرقام بين النص المصدر والنص الهدف.</li> | |
<li>انتبه للأرقام بمختلف أنظمة كتابتها (عربي، هندي، إنجليزي).</li> | |
<li>تحقق من تنسيق الأرقام مثل استخدام الفواصل العشرية.</li> | |
`; | |
} else if (errorType === 'missing') { | |
tips = ` | |
<li>أضف النص المفقود إلى الترجمة.</li> | |
<li>تأكد من ترجمة جميع عناصر النص المصدر بشكل كامل.</li> | |
<li>ابحث عن أي فقرات أو جمل منسية في النص الهدف.</li> | |
`; | |
} else if (errorType === 'meaning') { | |
tips = ` | |
<li>راجع معنى النص في المصدر والهدف للتأكد من التطابق.</li> | |
<li>استخدم مصطلحات دقيقة ومناسبة للسياق.</li> | |
<li>تجنب الترجمة الحرفية التي قد تؤدي إلى فقدان المعنى الأصلي.</li> | |
`; | |
} | |
if (tips) { | |
contentHTML += ` | |
<div class="p-4 rounded-lg bg-green-50 border border-green-200"> | |
<h3 class="text-xl font-bold mb-3 text-gray-800 flex items-center"> | |
<i class="fas fa-lightbulb text-yellow-500 ml-2"></i> نصائح للتصحيح: | |
</h3> | |
<ul class="list-disc pr-6 space-y-2 text-gray-700"> | |
${tips} | |
</ul> | |
</div> | |
`; | |
} | |
content.innerHTML = contentHTML; | |
// عرض النافذة المنبثقة | |
overlay.classList.add('show'); | |
popup.classList.add('show'); | |
} | |
/* ===================================== | |
إظهار/إخفاء مؤشر التحميل غير المتزامن | |
===================================== */ | |
function showLoadingIndicator(text = "جاري معالجة البيانات...", progress = "0%") { | |
const indicator = document.getElementById('asyncLoadingIndicator'); | |
const textElement = document.getElementById('asyncLoadingText'); | |
const progressElement = document.getElementById('asyncLoadingProgress'); | |
textElement.textContent = text; | |
progressElement.textContent = progress; | |
indicator.classList.add('active'); | |
} | |
function hideLoadingIndicator() { | |
const indicator = document.getElementById('asyncLoadingIndicator'); | |
indicator.classList.remove('active'); | |
} | |
function updateLoadingProgress(text, progress) { | |
const textElement = document.getElementById('asyncLoadingText'); | |
const progressElement = document.getElementById('asyncLoadingProgress'); | |
textElement.textContent = text; | |
progressElement.textContent = progress; | |
} | |
/* ===================================== | |
تصحيح النصوص للمستندات الرسمية - محسن لدعم القوالب | |
===================================== */ | |
async function correctOfficialDocument(text, targetType) { | |
try { | |
// الحصول على نوع القالب المحدد | |
const templateSelect = document.getElementById('docTemplate'); | |
const templateType = templateSelect ? templateSelect.value : 'other'; | |
// عرض حالة التصحيح | |
document.getElementById('correctionStatus').classList.add('active'); | |
document.getElementById('correctionMessage').textContent = 'جاري تحليل النص وتحديد الدولة...'; | |
document.getElementById('correctionProgressBar').style.width = '10%'; | |
// عرض مؤشر التحميل غير المتزامن | |
showLoadingIndicator('جاري تحليل المستند الرسمي وتصحيح الثوابت...', '10%'); | |
// حفظ النص الأصلي | |
originalTextBeforeCorrection = text; | |
// تحديد الدولة أولاً | |
detectedCountry = await detectCountryFromText(text); | |
document.getElementById('correctionMessage').textContent = `تم تحديد الدولة: ${detectedCountry}. جاري تطبيق قواعد التصحيح...`; | |
document.getElementById('correctionProgressBar').style.width = '40%'; | |
updateLoadingProgress(`تم تحديد الدولة: ${detectedCountry}. جاري تطبيق قواعد التصحيح...`, '40%'); | |
// تطبيق التصحيحات بناءً على الدولة ونوع القالب | |
const correctionResult = await applyCorrectionRulesWithTemplate(text, detectedCountry, templateType); | |
document.getElementById('correctionProgressBar').style.width = '80%'; | |
updateLoadingProgress('جاري تطبيق التصحيحات النهائية...', '80%'); | |
// حفظ النص المُصحح والتغييرات | |
correctedText = correctionResult.correctedText; | |
correctionChanges = correctionResult.changes; | |
document.getElementById('correctionMessage').textContent = `تم تطبيق ${correctionChanges.length} تصحيح بنجاح`; | |
document.getElementById('correctionProgressBar').style.width = '100%'; | |
updateLoadingProgress(`تم تطبيق ${correctionChanges.length} تصحيح بنجاح`, '100%'); | |
// إخفاء حالة التصحيح وعرض المعاينة | |
setTimeout(() => { | |
document.getElementById('correctionStatus').classList.remove('active'); | |
hideLoadingIndicator(); | |
showCorrectionPreview(); | |
}, 1000); | |
correctionApplied = true; | |
return correctedText; | |
} catch (error) { | |
console.error('خطأ في تصحيح المستند الرسمي:', error); | |
document.getElementById('correctionStatus').classList.remove('active'); | |
hideLoadingIndicator(); | |
addError('حدث خطأ أثناء تصحيح المستند الرسمي: ' + error.message, 'error'); | |
return text; // إرجاع النص الأصلي في حالة الخطأ | |
} | |
} | |
/* ===================================== | |
1. تطبيق قواعد التصحيح حسب القالب - محسن | |
===================================== */ | |
async function applyCorrectionRulesWithTemplate(text, country, templateType) { | |
try { | |
// تحديد القواعد الخاصة بنوع المستند المحدد | |
let templateRules = ''; | |
switch (templateType) { | |
case 'birth': | |
templateRules = ` | |
- "تارخ الميلاد" → "تاريخ الميلاد" | |
- "تارخ / مكان الميلاد" → "تاريخ / مكان الميلاد" | |
- "مكان المولد" → "مكان الميلاد" | |
- "الموليد" → "الميلاد" | |
- "الأولاذ" → "الأولاد" | |
- "رقم القد" → "رقم القيد" | |
- "رقم واقعة الميلاد" → "رقم واقعة الميلاد" | |
- "جحة الميلاد" → "جهة الميلاد" | |
- "شهاده ميلاد" → "شهادة ميلاد" | |
- "تارخ التحرير" → "تاريخ التحرير" | |
`; | |
break; | |
case 'id': | |
templateRules = ` | |
- "البطافة الشخصية" → "البطاقة الشخصية" | |
- "الرقو القومي" → "الرقم القومي" | |
- "الرقو الوطني" → "الرقم الوطني" | |
- "الرقو المدنى" → "الرقم المدني" | |
- "الرقو الموحد" → "الرقم الموحد" | |
- "الأثم" → "الاسم" | |
- "الأثم الكامل" → "الاسم الكامل" | |
- "تارخ الميلاد" → "تاريخ الميلاد" | |
- "محل الميلاذ" → "محل الميلاد" | |
- "الحنسية" → "الجنسية" | |
- "محل الإفامة" → "محل الإقامة" | |
- "العمل/المهنه" → "العمل/المهنة" | |
- "تارخ الأنتهاء" → "تاريخ الانتهاء" | |
- "تارخ الإصدار" → "تاريخ الإصدار" | |
- "البيناات" → "البيانات" | |
`; | |
break; | |
case 'passport': | |
templateRules = ` | |
- "جواز سفر" → "جواز سفر" | |
- "جوار سفر" → "جواز سفر" | |
- "الأثم" → "الاسم" | |
- "الشهره" → "الشهرة" | |
- "النوع/الحنس" → "النوع/الجنس" | |
- "تارخ الإصدار" → "تاريخ الإصدار" | |
- "صالح لغايت" → "صالح لغاية" | |
- "تارخ الانتهاء" → "تاريخ الانتهاء" | |
- "صلاحيه حتى" → "صلاحية حتى" | |
- "رقو الجواز" → "رقم الجواز" | |
- "السلطه المصدرة" → "السلطة المصدرة" | |
`; | |
break; | |
case 'driving': | |
templateRules = ` | |
- "رخصة فياده" → "رخصة قيادة" | |
- "رخصة قياده" → "رخصة قيادة" | |
- "تارخ الإصدار" → "تاريخ الإصدار" | |
- "الرقو" → "الرقم" | |
- "اسم المالك/صاخب الترخيص" → "اسم المالك/صاحب الترخيص" | |
- "ساريه المفعول حتى" → "سارية المفعول حتى" | |
- "صالحيه حتى" → "صالحية حتى" | |
- "الغئة" → "الفئة" | |
- "أقر بإستخدام النقارة" → "أقر باستخدام النظارة" | |
`; | |
break; | |
default: // other | |
templateRules = ` | |
- "الأثم" → "الاسم" | |
- "تارخ" → "تاريخ" | |
- "الموليد" → "المولد" | |
- "الحنسية" → "الجنسية" | |
- "الرقو" → "الرقم" | |
- "الهويه" → "الهوية" | |
- "البيناات" → "البيانات" | |
- "الشحصية" → "الشخصية" | |
- "اسم الشخس" → "اسم الشخص" | |
`; | |
break; | |
} | |
const prompt = `صحح الأخطاء في الثوابت فقط (وليس المتغيرات) في النص التالي من مستند رسمي ${country}ي من نوع "${getTemplateNameInArabic(templateType)}": | |
قواعد التصحيح: | |
1. صحح أخطاء الكلمات الثابتة مثل: | |
${templateRules} | |
2. صحح الأخطاء الإملائية في المصطلحات الرسمية حسب معايير ${country} | |
3. لا تُغيّر أي بيانات شخصية (الأسماء، التواريخ، الأرقام، العناوين) | |
4. احتفظ بالتنسيق الأصلي للنص تماماً | |
أعِد النص المُصحح مع قائمة بالتغييرات في هذا الشكل: | |
النص المُصحح: | |
[النص هنا] | |
التغييرات المطبقة: | |
- [الكلمة الخطأ] → [الكلمة الصحيحة] | |
النص الأصلي: | |
${text}`; | |
const payload = { | |
model: "deepseek-chat", | |
messages: [ | |
{ role: "system", content: "أنت خبير في تصحيح المستندات الرسمية العربية وتطبيق قواعد الإملاء والنحو المناسبة لكل دولة ونوع مستند" }, | |
{ role: "user", content: prompt } | |
], | |
temperature: 0.2, | |
max_tokens: 4000 | |
}; | |
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('فشل في تطبيق قواعد التصحيح'); | |
} | |
const data = await response.json(); | |
const result = data.choices[0].message.content.trim(); | |
// استخراج النص المُصحح والتغييرات | |
const correctedTextMatch = result.match(/النص المُصحح:\s*([\s\S]*?)(?=التغييرات المطبقة:|$)/); | |
const changesMatch = result.match(/التغييرات المطبقة:\s*([\s\S]*)/); | |
let correctedText = text; // افتراضي: النص الأصلي | |
let changes = []; | |
if (correctedTextMatch) { | |
correctedText = correctedTextMatch[1].trim(); | |
} | |
if (changesMatch) { | |
const changesText = changesMatch[1].trim(); | |
// استخراج التغييرات من النص | |
const changeLines = changesText.split('\n').filter(line => line.includes('→')); | |
changes = changeLines.map(line => { | |
const parts = line.replace(/^-\s*/, '').split('→'); | |
if (parts.length === 2) { | |
return { | |
original: parts[0].trim(), | |
corrected: parts[1].trim() | |
}; | |
} | |
return null; | |
}).filter(change => change !== null); | |
} | |
return { | |
correctedText: correctedText, | |
changes: changes | |
}; | |
} catch (error) { | |
console.error('خطأ في تطبيق قواعد التصحيح:', error); | |
throw error; | |
} | |
} | |
// 1. الحصول على اسم نوع القالب بالعربية | |
function getTemplateNameInArabic(templateType) { | |
switch (templateType) { | |
case 'birth': return 'شهادة ميلاد'; | |
case 'id': return 'بطاقة شخصية'; | |
case 'passport': return 'جواز سفر'; | |
case 'driving': return 'رخصة قيادة'; | |
default: return 'مستند رسمي'; | |
} | |
} | |
/* ===================================== | |
تحديد الدولة من النص - محسن | |
===================================== */ | |
async function detectCountryFromText(text) { | |
try { | |
const prompt = `حلل النص التالي وحدد الدولة التي ينتمي إليها هذا المستند الرسمي بناءً على: | |
1. المصطلحات المستخدمة | |
2. أسلوب الكتابة الرسمية | |
3. الكلمات المميزة للدولة | |
أجب بكلمة واحدة فقط اسم الدولة بالعربية. | |
النص: | |
${text.substring(0, 500)}`; | |
const payload = { | |
model: "deepseek-chat", | |
messages: [ | |
{ role: "system", content: "أنت خبير في تحديد مصدر المستندات الرسمية العربية" }, | |
{ role: "user", content: prompt } | |
], | |
temperature: 0.1, | |
max_tokens: 50 | |
}; | |
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('فشل في تحديد الدولة'); | |
} | |
const data = await response.json(); | |
const country = data.choices[0].message.content.trim(); | |
// تنظيف النتيجة للحصول على اسم الدولة فقط | |
const cleanCountry = country.replace(/[^\u0600-\u06FF\s]/g, '').trim(); | |
return cleanCountry || 'مصر'; // افتراضي: مصر | |
} catch (error) { | |
console.error('خطأ في تحديد الدولة:', error); | |
return 'مصر'; // قيمة افتراضية | |
} | |
} | |
/* ===================================== | |
عرض معاينة التصحيحات - محسنة | |
===================================== */ | |
function showCorrectionPreview() { | |
const previewElement = document.getElementById('correctionPreview'); | |
const changesElement = document.getElementById('correctionChanges'); | |
if (correctionChanges.length === 0) { | |
changesElement.innerHTML = '<p class="text-gray-500">لم يتم العثور على أخطاء تحتاج إلى تصحيح في الثوابت</p>'; | |
} else { | |
let changesHTML = ''; | |
correctionChanges.forEach(change => { | |
changesHTML += ` | |
<div class="mb-2"> | |
<span class="original-text">${change.original}</span> | |
<span class="mx-2">→</span> | |
<span class="corrected-text">${change.corrected}</span> | |
</div> | |
`; | |
}); | |
changesElement.innerHTML = changesHTML; | |
} | |
previewElement.classList.add('active'); | |
} | |
/* ===================================== | |
دالة المعالجة الرئيسية للملفات - محسنة | |
===================================== */ | |
function processFile(file, targetType) { | |
if (!file) return; | |
document.getElementById('processingStatus').classList.remove('hidden'); | |
document.getElementById('statusText').textContent = 'جاري فحص نوع الملف...'; | |
document.getElementById('progressBar').style.width = '10%'; | |
// عرض مؤشر التحميل غير المتزامن | |
showLoadingIndicator('جاري فحص وتجهيز الملف...', '10%'); | |
currentProcessingMode = targetType; | |
// تحديد نوع الملف وتوجيهه للمعالجة المناسبة | |
if (file.type === 'application/pdf') { | |
processPDF(file, targetType); | |
} else if (file.type.startsWith('image/')) { | |
processImage(file, targetType); | |
} else if (file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || | |
file.name.toLowerCase().endsWith('.docx')) { | |
processDocx(file, targetType); | |
} else if (file.type === 'application/vnd.ms-excel' || | |
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || | |
file.name.toLowerCase().endsWith('.xlsx') || | |
file.name.toLowerCase().endsWith('.xls')) { | |
processExcel(file, targetType); | |
} else { | |
// إذا كان نوع الملف غير مدعوم | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
alert('نوع الملف غير مدعوم. يرجى تحميل ملف PDF أو صورة أو Word أو Excel.'); | |
} | |
} | |
/* ===================================== | |
معالجة ملف PDF - محسنة | |
===================================== */ | |
async function processPDF(file, targetType) { | |
try { | |
document.getElementById('statusText').textContent = 'جاري معالجة ملف PDF...'; | |
document.getElementById('progressBar').style.width = '20%'; | |
updateLoadingProgress('جاري معالجة ملف PDF...', '20%'); | |
const arrayBuffer = await file.arrayBuffer(); | |
const loadingTask = pdfjsLib.getDocument(arrayBuffer); | |
const pdf = await loadingTask.promise; | |
documentPages = []; | |
selectedPages = []; | |
document.getElementById('progressBar').style.width = '50%'; | |
updateLoadingProgress('جاري استخراج صفحات PDF...', '50%'); | |
for (let i = 1; i <= pdf.numPages; i++) { | |
const page = await pdf.getPage(i); | |
const viewport = page.getViewport({ scale: 1.5 }); | |
const canvas = document.createElement('canvas'); | |
const context = canvas.getContext('2d'); | |
canvas.height = viewport.height; | |
canvas.width = viewport.width; | |
updateLoadingProgress(`جاري استخراج الصفحة ${i} من ${pdf.numPages}...`, `${Math.round(50 + (i/pdf.numPages) * 20)}%`); | |
await page.render({ | |
canvasContext: context, | |
viewport: viewport | |
}).promise; | |
documentPages.push({ | |
pageNum: i, | |
imageData: canvas.toDataURL('image/jpeg'), | |
selected: true // تحديد كل الصفحات تلقائيًا | |
}); | |
selectedPages.push(i-1); // إضافة الصفحة للصفحات المحددة | |
} | |
document.getElementById('progressBar').style.width = '70%'; | |
updateLoadingProgress('جاري تحضير الصفحات للعرض...', '70%'); | |
document.getElementById('pdfPagesCard').classList.remove('hidden'); | |
displayPDFPages(); | |
// بدء معالجة OCR تلقائيًا | |
document.getElementById('progressBar').style.width = '80%'; | |
updateLoadingProgress('جاري استخراج النص من الصفحات...', '80%'); | |
await extractText(); | |
} catch (error) { | |
console.error('خطأ في معالجة ملف PDF:', error); | |
alert('حدث خطأ أثناء معالجة ملف PDF'); | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
} | |
} | |
/* ===================================== | |
معالجة ملف صورة - محسنة | |
===================================== */ | |
function processImage(file, targetType) { | |
document.getElementById('statusText').textContent = 'جاري معالجة الصورة...'; | |
document.getElementById('progressBar').style.width = '30%'; | |
updateLoadingProgress('جاري معالجة الصورة...', '30%'); | |
const reader = new FileReader(); | |
reader.onload = async function(e) { | |
const img = new Image(); | |
img.onload = async function() { | |
documentPages = [{ | |
pageNum: 1, | |
imageData: e.target.result, | |
selected: true | |
}]; | |
selectedPages = [0]; | |
document.getElementById('progressBar').style.width = '60%'; | |
updateLoadingProgress('جاري تحضير الصورة للعرض والمعالجة...', '60%'); | |
document.getElementById('pdfPagesCard').classList.remove('hidden'); | |
displayPDFPages(); | |
// بدء معالجة OCR تلقائيًا | |
document.getElementById('progressBar').style.width = '80%'; | |
updateLoadingProgress('جاري استخراج النص من الصورة...', '80%'); | |
await extractText(); | |
}; | |
img.src = e.target.result; | |
}; | |
reader.onerror = function() { | |
console.error('خطأ في قراءة الصورة'); | |
alert('حدث خطأ أثناء قراءة الصورة'); | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
}; | |
reader.readAsDataURL(file); | |
} | |
/* ===================================== | |
معالجة ملف Word (DOCX) - محسنة | |
===================================== */ | |
async function processDocx(file, targetType) { | |
try { | |
document.getElementById('statusText').textContent = 'جاري معالجة ملف Word...'; | |
document.getElementById('progressBar').style.width = '30%'; | |
updateLoadingProgress('جاري معالجة ملف Word...', '30%'); | |
const arrayBuffer = await file.arrayBuffer(); | |
// تحسين استخراج النص من DOCX مع محاولة معالجة المستندات الرسمية | |
updateLoadingProgress('جاري استخراج النص من ملف Word...', '50%'); | |
try { | |
// استخدام مكتبة Mammoth لاستخراج النص من DOCX | |
const result = await mammoth.extractRawText({ arrayBuffer: arrayBuffer }); | |
document.getElementById('progressBar').style.width = '70%'; | |
if (result && result.value) { | |
let extractedText = result.value; | |
updateLoadingProgress('تم استخراج النص بنجاح!', '80%'); | |
// تطبيق التصحيح إذا كان الملف مستنداً رسمياً | |
if (selectedFileType === 'official') { | |
updateLoadingProgress('جاري تصحيح المستند الرسمي...', '85%'); | |
extractedText = await correctOfficialDocument(extractedText, targetType); | |
} | |
// تحديث شريط التقدم | |
document.getElementById('progressBar').style.width = '90%'; | |
updateLoadingProgress('جاري تحضير النتائج...', '90%'); | |
// عرض النص المستخرج | |
displayDocxExtractedText(extractedText); | |
// استخدام النص كمصدر أو هدف | |
if (targetType === 'source') { | |
document.getElementById('sourceText').value = extractedText; | |
addError('تم استخراج النص من ملف Word وإضافته كنص مصدر', 'info'); | |
} else if (targetType === 'target') { | |
document.getElementById('targetText').value = extractedText; | |
addError('تم استخراج النص من ملف Word وإضافته كنص هدف', 'info'); | |
} else if (targetType === 'extra') { | |
document.getElementById('sourceExtraText').value = extractedText; | |
addError('تم استخراج النص من ملف Word وإضافته كمصدر إضافي', 'info'); | |
} | |
document.getElementById('progressBar').style.width = '100%'; | |
updateLoadingProgress('تمت العملية بنجاح!', '100%'); | |
setTimeout(() => { | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
}, 1000); | |
} else { | |
throw new Error('فشل في استخراج النص من الملف'); | |
} | |
} catch (mammothError) { | |
console.error('خطأ في استخراج النص باستخدام Mammoth:', mammothError); | |
// محاولة استخدام طريقة بديلة لاستخراج النص | |
updateLoadingProgress('جاري محاولة استخراج النص بطريقة بديلة...', '60%'); | |
try { | |
// استخدام OCR كآلية احتياطية لاستخراج النص | |
// تحويل الـ DOCX إلى صورة أولاً | |
const docxContent = new Uint8Array(arrayBuffer); | |
const blob = new Blob([docxContent], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }); | |
// استخراج نص بديل | |
const fallbackText = await extractFallbackTextFromDocx(blob); | |
if (fallbackText) { | |
let extractedText = fallbackText; | |
// تطبيق التصحيح إذا كان الملف مستنداً رسمياً | |
if (selectedFileType === 'official') { | |
updateLoadingProgress('جاري تصحيح المستند الرسمي...', '85%'); | |
extractedText = await correctOfficialDocument(extractedText, targetType); | |
} | |
// عرض النص المستخرج | |
displayDocxExtractedText(extractedText); | |
// استخدام النص كمصدر أو هدف | |
if (targetType === 'source') { | |
document.getElementById('sourceText').value = extractedText; | |
addError('تم استخراج النص من ملف Word وإضافته كنص مصدر (باستخدام طريقة بديلة)', 'info'); | |
} else if (targetType === 'target') { | |
document.getElementById('targetText').value = extractedText; | |
addError('تم استخراج النص من ملف Word وإضافته كنص هدف (باستخدام طريقة بديلة)', 'info'); | |
} else if (targetType === 'extra') { | |
document.getElementById('sourceExtraText').value = extractedText; | |
addError('تم استخراج النص من ملف Word وإضافته كمصدر إضافي (باستخدام طريقة بديلة)', 'info'); | |
} | |
document.getElementById('progressBar').style.width = '100%'; | |
updateLoadingProgress('تمت العملية بنجاح!', '100%'); | |
setTimeout(() => { | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
}, 1000); | |
} else { | |
throw new Error('فشل في استخراج النص بالطريقة البديلة'); | |
} | |
} catch (fallbackError) { | |
console.error('خطأ في استخراج النص بالطريقة البديلة:', fallbackError); | |
throw new Error('فشل في استخراج النص من الملف بكلتا الطريقتين'); | |
} | |
} | |
} catch (error) { | |
console.error('خطأ في معالجة ملف Word:', error); | |
alert('حدث خطأ أثناء معالجة ملف Word: ' + error.message); | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
} | |
} | |
/* ===================================== | |
استخراج نص بديل من ملف Word | |
===================================== */ | |
async function extractFallbackTextFromDocx(blob) { | |
try { | |
// يتم استخدام هذه الدالة كبديل عندما تفشل مكتبة Mammoth | |
// نستخدم هنا خوارزمية مبسطة لاستخراج النص من ملف DOCX | |
// تحويل البلوب إلى أراي بفر | |
const arrayBuffer = await blob.arrayBuffer(); | |
const data = new Uint8Array(arrayBuffer); | |
// بحث عن سلاسل النصوص في ملف DOCX (مضغوط) | |
const textParts = []; | |
let currentText = ''; | |
// البحث عن النصوص العربية والإنجليزية | |
const arabicOrEnglishRegex = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF\uFB50-\uFDFF\uFE70-\uFEFF\s\da-zA-Z.,;:'"!@#$%^&*()_+-=[\]{}|<>/?\\~`]+/g; | |
// تحويل الباينري إلى نص | |
const text = String.fromCharCode.apply(null, data); | |
// استخراج الأجزاء النصية | |
const matches = text.match(arabicOrEnglishRegex); | |
if (matches && matches.length > 0) { | |
// تجميع النصوص المستخرجة | |
return matches.join(' ').replace(/\s+/g, ' ').trim(); | |
} | |
return null; | |
} catch (error) { | |
console.error('خطأ في استخراج النص البديل:', error); | |
return null; | |
} | |
} | |
/* ===================================== | |
عرض النص المستخرج من ملف Word - محسنة | |
===================================== */ | |
function displayDocxExtractedText(text) { | |
// هنا يمكن إضافة معالجة إضافية للنص المستخرج إذا لزم الأمر | |
document.getElementById('resultsCard').classList.remove('hidden'); | |
document.getElementById('pdfPagesCard').classList.add('hidden'); | |
document.getElementById('resultPreview').innerHTML = ` | |
<div class="page-preview"> | |
<h4>النص المستخرج من ملف Word${correctionApplied ? ' (مُصحح)' : ''}</h4> | |
<div>${text.substring(0, 200)}${text.length > 200 ? '...' : ''}</div> | |
</div> | |
`; | |
document.getElementById('resultText').textContent = text; | |
} | |
/* ===================================== | |
معالجة ملف Excel - محسنة | |
===================================== */ | |
function processExcel(file, targetType) { | |
document.getElementById('statusText').textContent = 'جاري معالجة ملف Excel...'; | |
document.getElementById('progressBar').style.width = '30%'; | |
updateLoadingProgress('جاري معالجة ملف Excel...', '30%'); | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
try { | |
const data = new Uint8Array(e.target.result); | |
excelWorkbook = XLSX.read(data, {type: 'array'}); | |
// تحديث شريط التقدم | |
document.getElementById('progressBar').style.width = '70%'; | |
updateLoadingProgress('جاري استخراج البيانات من ملف Excel...', '70%'); | |
if (excelWorkbook.SheetNames.length > 0) { | |
// عرض قائمة بأسماء الأوراق | |
renderSheetSelector(excelWorkbook.SheetNames, targetType); | |
// اختيار الورقة الأولى تلقائيًا | |
selectExcelSheet(excelWorkbook.SheetNames[0], targetType); | |
updateLoadingProgress('تم استخراج البيانات بنجاح!', '90%'); | |
// استخدام النص كمصدر أو هدف تلقائيًا | |
useExcelContent(targetType); | |
} else { | |
alert('لم يتم العثور على أوراق في ملف Excel'); | |
hideLoadingIndicator(); | |
} | |
document.getElementById('progressBar').style.width = '100%'; | |
updateLoadingProgress('تمت العملية بنجاح!', '100%'); | |
setTimeout(() => { | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
}, 500); | |
} catch (error) { | |
console.error('خطأ في معالجة ملف Excel:', error); | |
alert('حدث خطأ أثناء معالجة ملف Excel'); | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
} | |
}; | |
reader.onerror = function() { | |
alert('حدث خطأ أثناء قراءة الملف'); | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
}; | |
reader.readAsArrayBuffer(file); | |
} | |
/* ===================================== | |
عرض صفحات PDF - محسنة | |
===================================== */ | |
function displayPDFPages() { | |
const container = document.getElementById('pdfPagesContainer'); | |
container.innerHTML = ''; | |
documentPages.forEach((page, index) => { | |
const pageDiv = document.createElement('div'); | |
pageDiv.className = `pdf-page ${page.selected ? 'selected' : ''}`; | |
pageDiv.dataset.index = index; | |
const img = document.createElement('img'); | |
img.src = page.imageData; | |
img.alt = `Page ${page.pageNum}`; | |
const pageNumDiv = document.createElement('div'); | |
pageNumDiv.className = 'page-number'; | |
pageNumDiv.textContent = page.pageNum; | |
pageDiv.appendChild(img); | |
pageDiv.appendChild(pageNumDiv); | |
container.appendChild(pageDiv); | |
pageDiv.addEventListener('click', function() { | |
this.classList.toggle('selected'); | |
documentPages[index].selected = !documentPages[index].selected; | |
if (documentPages[index].selected) { | |
if (!selectedPages.includes(index)) { | |
selectedPages.push(index); | |
} | |
} else { | |
const pos = selectedPages.indexOf(index); | |
if (pos !== -1) { | |
selectedPages.splice(pos, 1); | |
} | |
} | |
}); | |
}); | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
} | |
/* ===================================== | |
تحديد/إلغاء تحديد كل الصفحات | |
===================================== */ | |
function selectAllPages() { | |
selectedPages = []; | |
documentPages.forEach((page, index) => { | |
page.selected = true; | |
selectedPages.push(index); | |
}); | |
document.querySelectorAll('.pdf-page').forEach(pageDiv => { | |
pageDiv.classList.add('selected'); | |
}); | |
} | |
function deselectAllPages() { | |
selectedPages = []; | |
documentPages.forEach(page => { | |
page.selected = false; | |
}); | |
document.querySelectorAll('.pdf-page').forEach(pageDiv => { | |
pageDiv.classList.remove('selected'); | |
}); | |
} | |
/* ===================================== | |
استخراج النص من الصفحات المحددة - محسنة بشكل كبير | |
===================================== */ | |
async function extractText() { | |
if (selectedPages.length === 0) { | |
alert('الرجاء اختيار صفحة واحدة على الأقل'); | |
return; | |
} | |
document.getElementById('processingStatus').classList.remove('hidden'); | |
document.getElementById('statusText').textContent = 'جاري استخراج النص...'; | |
document.getElementById('progressBar').style.width = '0%'; | |
showLoadingIndicator('جاري تحضير الصفحات لاستخراج النص...', '0%'); | |
extractedTexts = []; | |
extractedPageNumbers = []; | |
try { | |
for (let i = 0; i < selectedPages.length; i++) { | |
const pageIndex = selectedPages[i]; | |
const pageData = documentPages[pageIndex]; | |
document.getElementById('statusText').textContent = `جاري استخراج النص من الصفحة ${pageData.pageNum}...`; | |
document.getElementById('progressBar').style.width = `${(i / selectedPages.length) * 70}%`; | |
updateLoadingProgress(`جاري استخراج النص من الصفحة ${pageData.pageNum} (${i+1} من ${selectedPages.length})...`, | |
`${Math.round((i / selectedPages.length) * 70)}%`); | |
// استخدام API الـ OCR بشكل غير متزامن | |
const extractedText = await extractTextFromImage(pageData.imageData, pageData.pageNum); | |
extractedTexts.push(extractedText); | |
extractedPageNumbers.push(pageData.pageNum); | |
// تحديث العداد وتاريخ آخر معالجة في localStorage | |
ocrPagesCount++; | |
localStorage.setItem('ocrPagesCount', ocrPagesCount); | |
document.getElementById('ocrCounter').textContent = ocrPagesCount; | |
const now = new Date(); | |
const dateStr = now.toLocaleDateString('ar-EG'); | |
localStorage.setItem('lastOcrDate', dateStr); | |
document.getElementById('lastOcrDate').textContent = dateStr; | |
} | |
document.getElementById('progressBar').style.width = '80%'; | |
updateLoadingProgress('جاري معالجة النصوص المستخرجة...', '80%'); | |
// دمج النصوص المستخرجة | |
let combinedText = extractedTexts.join('\n\n'); | |
// تطبيق التصحيح إذا كان الملف مستنداً رسمياً | |
if (selectedFileType === 'official') { | |
updateLoadingProgress('جاري تصحيح النص المستخرج للمستند الرسمي...', '85%'); | |
combinedText = await correctOfficialDocument(combinedText, currentProcessingMode); | |
} | |
updateLoadingProgress('جاري تحضير النتائج...', '90%'); | |
// عرض النصوص المستخرجة | |
displayExtractedTexts(); | |
// استخدام النص تلقائيًا | |
if (currentProcessingMode === 'source') { | |
document.getElementById('sourceText').value = combinedText; | |
addError('تم إضافة النص المستخرج كنص مصدر بنجاح', 'info'); | |
} else if (currentProcessingMode === 'target') { | |
document.getElementById('targetText').value = combinedText; | |
addError('تم إضافة النص المستخرج كنص هدف بنجاح', 'info'); | |
} else if (currentProcessingMode === 'extra') { | |
document.getElementById('sourceExtraText').value = combinedText; | |
addError('تم إضافة النص المستخرج كمصدر إضافي بنجاح', 'info'); | |
} | |
updateLoadingProgress('تمت العملية بنجاح!', '100%'); | |
document.getElementById('progressBar').style.width = '100%'; | |
setTimeout(() => { | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
}, 1000); | |
} catch (error) { | |
console.error('خطأ في استخراج النص:', error); | |
alert('حدث خطأ أثناء استخراج النص: ' + error.message); | |
document.getElementById('processingStatus').classList.add('hidden'); | |
hideLoadingIndicator(); | |
} | |
} | |
/* ===================================== | |
استخراج النص من صورة - محسنة | |
===================================== */ | |
async function extractTextFromImage(imageData, pageNumber) { | |
try { | |
// تحويل Data URL إلى Blob | |
const response = await fetch(imageData); | |
const blob = await response.blob(); | |
// إنشاء FormData وإضافة الصورة | |
const formData = new FormData(); | |
formData.append('image', blob); | |
formData.append('language', 'ara'); // تحديد اللغة العربية | |
// طلب OCR | |
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 { | |
// استخراج النص من الاستجابة مع الحفاظ على تنسيق الصفوف | |
if (data && data.results && data.results[0] && data.results[0].entities && | |
data.results[0].entities[0] && data.results[0].entities[0].objects && | |
data.results[0].entities[0].objects[0] && data.results[0].entities[0].objects[0].entities && | |
data.results[0].entities[0].objects[0].entities[0] && data.results[0].entities[0].objects[0].entities[0].text) { | |
const text = data.results[0].entities[0].objects[0].entities[0].text; | |
return text; | |
} else { | |
// محاولة استخراج النص بطريقة بديلة | |
if (data && data.results && data.results[0] && data.results[0].text) { | |
return data.results[0].text; | |
} | |
return `[لم يتم العثور على نص في الصفحة ${pageNumber}]`; | |
} | |
} catch (e) { | |
console.error('Error parsing OCR response:', e); | |
return `[خطأ في معالجة النص للصفحة ${pageNumber}]`; | |
} | |
} catch (error) { | |
console.error(`Error in OCR for page ${pageNumber}:`, error); | |
throw error; | |
} | |
} | |
/* ===================================== | |
عرض النصوص المستخرجة - محسنة | |
===================================== */ | |
function displayExtractedTexts() { | |
document.getElementById('pdfPagesCard').classList.add('hidden'); | |
document.getElementById('resultsCard').classList.remove('hidden'); | |
document.getElementById('processingStatus').classList.add('hidden'); | |
const resultPreview = document.getElementById('resultPreview'); | |
resultPreview.innerHTML = ''; | |
extractedTexts.forEach((text, index) => { | |
const pagePreview = document.createElement('div'); | |
pagePreview.className = 'page-preview'; | |
pagePreview.innerHTML = ` | |
<h4>الصفحة ${extractedPageNumbers[index]}${correctionApplied ? ' (مُصححة)' : ''}</h4> | |
<div>${text.substring(0, 100)}${text.length > 100 ? '...' : ''}</div> | |
`; | |
resultPreview.appendChild(pagePreview); | |
}); | |
// عرض النص الكامل (المُصحح إذا لزم الأمر) | |
const finalText = correctionApplied ? correctedText : extractedTexts.join('\n\n'); | |
document.getElementById('resultText').textContent = finalText; | |
} | |
/* ===================================== | |
نسخ النص المستخرج | |
===================================== */ | |
function copyText() { | |
const resultText = document.getElementById('resultText'); | |
// إنشاء عنصر textarea مؤقت | |
const textarea = document.createElement('textarea'); | |
textarea.value = resultText.textContent; | |
document.body.appendChild(textarea); | |
// اختيار النص ونسخه | |
textarea.select(); | |
document.execCommand('copy'); | |
// إزالة العنصر المؤقت | |
document.body.removeChild(textarea); | |
alert('تم نسخ النص'); | |
} | |
/* ===================================== | |
تنزيل النص المستخرج | |
===================================== */ | |
function downloadText() { | |
const text = document.getElementById('resultText').textContent; | |
const blob = new Blob([text], { type: 'text/plain' }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = 'extracted_text.txt'; | |
a.click(); | |
URL.revokeObjectURL(url); | |
} | |
/* ===================================== | |
استخدام النص المستخرج - محسنة | |
===================================== */ | |
function useOcrText(target) { | |
const text = document.getElementById('resultText').textContent; | |
if (target === 'source') { | |
document.getElementById('sourceText').value = text; | |
} else { | |
document.getElementById('targetText').value = text; | |
} | |
const message = `تم استخدام النص المستخرج${correctionApplied ? ' المُصحح' : ''} كنص ${target === 'source' ? 'مصدر' : 'هدف'}`; | |
alert(message); | |
} | |
/* ===================================== | |
وظائف محرر الصور | |
===================================== */ | |
function initializeImageEditor(index) { | |
currentPageIndex = index; | |
const imageCanvas = document.getElementById('imageCanvas'); | |
const imageEditor = document.getElementById('imageEditor'); | |
imageEditor.classList.remove('hidden'); | |
// حفظ البيانات الأصلية للصورة | |
originalImageData = documentPages[index].imageData; | |
// إنشاء كائن Fabric canvas | |
if (fabricCanvas) { | |
fabricCanvas.dispose(); | |
} | |
fabricCanvas = new fabric.Canvas('imageCanvas'); | |
// تحميل الصورة | |
fabric.Image.fromURL(originalImageData, function(img) { | |
// تحجيم الصورة لتناسب الكانفاس | |
const containerWidth = imageCanvas.parentElement.clientWidth; | |
const scale = containerWidth / img.width; | |
img.scale(scale); | |
fabricCanvas.setWidth(img.width * scale); | |
fabricCanvas.setHeight(img.height * scale); | |
fabricCanvas.setBackgroundImage(img, fabricCanvas.renderAll.bind(fabricCanvas)); | |
}); | |
} | |
function rotateImageLeft() { | |
if (!fabricCanvas) return; | |
const img = fabricCanvas.backgroundImage; | |
img.rotate((img.angle || 0) - 90); | |
fabricCanvas.renderAll(); | |
updateModifiedImage(); | |
} | |
function rotateImageRight() { | |
if (!fabricCanvas) return; | |
const img = fabricCanvas.backgroundImage; | |
img.rotate((img.angle || 0) + 90); | |
fabricCanvas.renderAll(); | |
updateModifiedImage(); | |
} | |
function flipImageHorizontal() { | |
if (!fabricCanvas) return; | |
const img = fabricCanvas.backgroundImage; | |
img.set('flipX', !img.flipX); | |
fabricCanvas.renderAll(); | |
updateModifiedImage(); | |
} | |
function flipImageVertical() { | |
if (!fabricCanvas) return; | |
const img = fabricCanvas.backgroundImage; | |
img.set('flipY', !img.flipY); | |
fabricCanvas.renderAll(); | |
updateModifiedImage(); | |
} | |
function activateCropMode() { | |
if (!fabricCanvas) return; | |
if (isInCropMode) { | |
return; | |
} | |
isInCropMode = true; | |
document.getElementById('cropImage').textContent = 'تطبيق القص'; | |
// إنشاء مربع للقص | |
cropRect = new fabric.Rect({ | |
left: 50, | |
top: 50, | |
width: fabricCanvas.width - 100, | |
height: fabricCanvas.height - 100, | |
fill: 'rgba(0,0,0,0.2)', | |
stroke: 'black', | |
strokeDashArray: [5, 5], | |
strokeWidth: 2, | |
selectable: true | |
}); | |
fabricCanvas.add(cropRect); | |
fabricCanvas.setActiveObject(cropRect); | |
} | |
function applyCrop() { | |
if (!fabricCanvas || !cropRect) return; | |
isInCropMode = false; | |
document.getElementById('cropImage').textContent = 'قص الصورة'; | |
// الحصول على معلومات مستطيل القص | |
const rect = cropRect; | |
const img = fabricCanvas.backgroundImage; | |
// إزالة مستطيل القص | |
fabricCanvas.remove(rect); | |
// إنشاء كانفاس جديد للقص | |
const cropCanvas = document.createElement('canvas'); | |
const cropContext = cropCanvas.getContext('2d'); | |
cropCanvas.width = rect.getScaledWidth(); | |
cropCanvas.height = rect.getScaledHeight(); | |
// إعداد المصدر والوجهة للقص | |
const sourceLeft = rect.left; | |
const sourceTop = rect.top; | |
const sourceWidth = rect.getScaledWidth(); | |
const sourceHeight = rect.getScaledHeight(); | |
// إنشاء صورة جديدة من الصورة الأصلية | |
const tempImage = new Image(); | |
tempImage.onload = function() { | |
// رسم الجزء المقصوص من الصورة | |
cropContext.drawImage( | |
tempImage, | |
sourceLeft / img.scaleX, | |
sourceTop / img.scaleY, | |
sourceWidth / img.scaleX, | |
sourceHeight / img.scaleY, | |
0, 0, | |
cropCanvas.width, | |
cropCanvas.height | |
); | |
// تحديث صورة الخلفية | |
fabric.Image.fromURL(cropCanvas.toDataURL(), function(newImg) { | |
fabricCanvas.setBackgroundImage(newImg, fabricCanvas.renderAll.bind(fabricCanvas)); | |
fabricCanvas.setDimensions({ | |
width: cropCanvas.width, | |
height: cropCanvas.height | |
}); | |
updateModifiedImage(); | |
}); | |
}; | |
tempImage.src = img._element.src; | |
} | |
function resetImage() { | |
if (!fabricCanvas) return; | |
// إعادة الصورة إلى حالتها الأصلية | |
fabric.Image.fromURL(originalImageData, function(img) { | |
const containerWidth = document.getElementById('imageCanvas').parentElement.clientWidth; | |
const scale = containerWidth / img.width; | |
img.scale(scale); | |
fabricCanvas.setWidth(img.width * scale); | |
fabricCanvas.setHeight(img.height * scale); | |
fabricCanvas.setBackgroundImage(img, fabricCanvas.renderAll.bind(fabricCanvas)); | |
// إزالة أي عناصر إضافية | |
fabricCanvas.clear(); | |
// إعادة تعيين حالة القص | |
isInCropMode = false; | |
cropRect = null; | |
document.getElementById('cropImage').textContent = 'قص الصورة'; | |
// تحديث الصورة المعدلة | |
updateModifiedImage(); | |
}); | |
} | |
function improveContrast() { | |
if (!fabricCanvas) return; | |
const img = fabricCanvas.backgroundImage; | |
// إنشاء كانفاس مؤقت | |
const tempCanvas = document.createElement('canvas'); | |
const tempContext = tempCanvas.getContext('2d'); | |
tempCanvas.width = img.width * img.scaleX; | |
tempCanvas.height = img.height * img.scaleY; | |
// رسم الصورة على الكانفاس المؤقت | |
tempContext.drawImage(img._element, 0, 0, tempCanvas.width, tempCanvas.height); | |
// الحصول على بيانات الصورة | |
const imageData = tempContext.getImageData(0, 0, tempCanvas.width, tempCanvas.height); | |
const data = imageData.data; | |
// تحسين التباين | |
const factor = 1.5; // عامل التباين | |
for (let i = 0; i < data.length; i += 4) { | |
// الحصول على القيم RGB | |
const r = data[i]; | |
const g = data[i + 1]; | |
const b = data[i + 2]; | |
// حساب القيمة الجديدة | |
data[i] = Math.min(255, Math.max(0, factor * (r - 128) + 128)); | |
data[i + 1] = Math.min(255, Math.max(0, factor * (g - 128) + 128)); | |
data[i + 2] = Math.min(255, Math.max(0, factor * (b - 128) + 128)); | |
} | |
// وضع البيانات المعدلة في الكانفاس | |
tempContext.putImageData(imageData, 0, 0); | |
// تحديث صورة الخلفية | |
fabric.Image.fromURL(tempCanvas.toDataURL(), function(newImg) { | |
fabricCanvas.setBackgroundImage(newImg, fabricCanvas.renderAll.bind(fabricCanvas)); | |
updateModifiedImage(); | |
}); | |
} | |
function updateModifiedImage() { | |
// تحديث الصورة المعدلة في مصفوفة الصفحات | |
if (fabricCanvas && currentPageIndex !== -1) { | |
documentPages[currentPageIndex].imageData = fabricCanvas.toDataURL(); | |
} | |
} | |
/* ===================================== | |
رندر أزرار اختيار أوراق Excel | |
===================================== */ | |
function renderSheetSelector(sheetNames, targetType) { | |
const container = document.getElementById('sheetSelectorContainer'); | |
container.innerHTML = ''; | |
sheetNames.forEach(sheetName => { | |
const btn = document.createElement('div'); | |
btn.className = 'sheet-selector'; | |
btn.textContent = sheetName; | |
btn.onclick = function() { | |
document.querySelectorAll('.sheet-selector').forEach(b => b.classList.remove('active')); | |
this.classList.add('active'); | |
selectExcelSheet(sheetName, targetType); | |
}; | |
container.appendChild(btn); | |
}); | |
// تفعيل الورقة الأولى | |
const firstSheet = container.querySelector('.sheet-selector'); | |
if (firstSheet) { | |
firstSheet.classList.add('active'); | |
} | |
// إظهار قسم معاينة Excel | |
document.getElementById('excelPreviewCard').classList.remove('hidden'); | |
} | |
/* ===================================== | |
اختيار ورقة Excel | |
===================================== */ | |
function selectExcelSheet(sheetName, targetType) { | |
currentSheetName = sheetName; | |
// تحويل الورقة إلى مصفوفة | |
const worksheet = excelWorkbook.Sheets[sheetName]; | |
const data = XLSX.utils.sheet_to_json(worksheet, {header: 1}); | |
// تخزين البيانات للاستخدام لاحقًا | |
excelData = { | |
targetType: targetType, | |
sheetName: sheetName, | |
data: data | |
}; | |
// عرض البيانات في جدول | |
renderExcelTable(data); | |
} | |
/* ===================================== | |
رندر جدول Excel | |
===================================== */ | |
function renderExcelTable(data) { | |
const container = document.getElementById('excelContent'); | |
if (!data || data.length === 0) { | |
container.innerHTML = '<p class="text-center p-4 text-gray-500">لا توجد بيانات في هذه الورقة</p>'; | |
return; | |
} | |
let html = '<table class="preview-table"><thead><tr>'; | |
// إنشاء الترويسة باستخدام الصف الأول أو محتوى عمود | |
const headerRow = data[0]; | |
for (let i = 0; i < headerRow.length; i++) { | |
html += `<th>${headerRow[i] || 'عمود ' + (i+1)}</th>`; | |
} | |
html += '</tr></thead><tbody>'; | |
// إضافة صفوف البيانات | |
for (let i = 1; i < data.length; i++) { | |
html += '<tr>'; | |
const row = data[i]; | |
for (let j = 0; j < headerRow.length; j++) { | |
html += `<td>${row[j] !== undefined ? row[j] : ''}</td>`; | |
} | |
html += '</tr>'; | |
} | |
html += '</tbody></table>'; | |
container.innerHTML = html; | |
} | |
/* ===================================== | |
استخدام محتوى Excel كنص | |
===================================== */ | |
function useExcelContent(target) { | |
if (!excelData || !excelData.data) { | |
alert('لم يتم تحميل بيانات Excel'); | |
return; | |
} | |
// تحويل البيانات إلى نص | |
let textContent = ''; | |
// تخطي صف الترويسة وبدء من الصف 1 | |
for (let i = 1; i < excelData.data.length; i++) { | |
const row = excelData.data[i]; | |
if (row && row.length > 0) { | |
textContent += row.join('\t') + '\n'; | |
} | |
} | |
// تحديث النص المصدر أو الهدف | |
if (target === 'source') { | |
document.getElementById('sourceText').value = textContent; | |
} else if (target === 'target') { | |
document.getElementById('targetText').value = textContent; | |
} else if (target === 'extra') { | |
document.getElementById('sourceExtraText').value = textContent; | |
} | |
alert('تم استخدام محتوى Excel بنجاح'); | |
} | |
/* ===================================== | |
دالة توحيد الأرقام بمختلف أشكالها | |
===================================== */ | |
function normalizeNumbers(text) { | |
// تحويل الأرقام العربية والهندية إلى أرقام إنجليزية | |
return text.replace(/[٠١٢٣٤٥٦٧٨٩]/g, d => d.charCodeAt(0) - 1632) // أرقام عربية | |
.replace(/[۰۱۲۳۴۵۶۷۸۹]/g, d => d.charCodeAt(0) - 1776); // أرقام هندية | |
} | |
/* ===================================== | |
تقسيم النص إلى جمل - محسن | |
===================================== */ | |
function splitIntoSentences(text) { | |
// تعبير منتظم محسن للتعامل مع نهايات الجمل بشكل أفضل | |
const sentences = text.split(/(?<=[.!?])\s+|(?<=\n\s*\n)/g); | |
// تنظيف النتائج من الجمل الفارغة | |
return sentences.filter(s => s.trim()); | |
} | |
/* ===================================== | |
دالة معالجة عملية التحليل عند الضغط على الزر - محسنة مع غير متزامنة | |
===================================== */ | |
document.getElementById('submitBtn').addEventListener('click', async () => { | |
try { | |
let sourceText = document.getElementById('sourceText').value; | |
let targetText = document.getElementById('targetText').value; | |
// مسح الرسائل السابقة وإظهار النتائج | |
document.getElementById('errorsList').innerHTML = ''; | |
document.getElementById('resultSection').classList.remove('hidden'); | |
if (!sourceText || !targetText) { | |
addError('يرجى إدخال كلا النصين المصدر والهدف'); | |
return; | |
} | |
// عرض مؤشر التحميل | |
showLoadingIndicator('جاري تحضير النصوص للتحليل...', '0%'); | |
// تطبيق التصحيح على النصوص إذا كانت مستندات رسمية | |
if (selectedFileType === 'official') { | |
addError('جارٍ تصحيح النصوص للمستندات الرسمية...', 'info'); | |
updateLoadingProgress('جاري تصحيح النص المصدر...', '5%'); | |
// تصحيح النص المصدر إذا لم يتم تصحيحه من قبل | |
if (!correctionApplied) { | |
sourceText = await correctOfficialDocument(sourceText, 'source'); | |
document.getElementById('sourceText').value = sourceText; | |
} | |
updateLoadingProgress('جاري تصحيح النص الهدف...', '20%'); | |
// تصحيح النص الهدف | |
targetText = await correctOfficialDocument(targetText, 'target'); | |
document.getElementById('targetText').value = targetText; | |
addError('تم تصحيح النصوص بنجاح', 'info'); | |
} | |
addError('جارٍ تقسيم النصوص ومزامنتها...', 'info'); | |
updateLoadingProgress('جاري تقسيم النصوص ومزامنتها...', '30%'); | |
// تقسيم النصوص إلى أجزاء متزامنة بحد أقصى 6 أقسام | |
const segments = await alignTextsWithModel(sourceText, targetText); | |
if (!segments || segments.length === 0) { | |
throw new Error('فشل في تقسيم النصوص - لم يتم العثور على أقسام'); | |
} | |
// عرض الأقسام في المسودة مع التمييز المبدئي | |
displayDraftSegments(segments); | |
// تخزين الأقسام في المتغير العام | |
analysisSegments = segments.map(segment => ({ | |
source: segment.source, | |
target: segment.target, | |
analysis: 'جارٍ التحليل...', | |
errors: { numbers: 0, missing: 0, meaning: 0 } | |
})); | |
addError(`تم تقسيم النصوص إلى ${segments.length} قسم بنجاح`, 'info'); | |
addError('جارٍ تحليل الأقسام...', 'info'); | |
updateLoadingProgress('جاري تحليل الأقسام...', '50%'); | |
let totalNumberErrors = 0; | |
let totalMissingErrors = 0; | |
let totalMeaningErrors = 0; | |
// تحليل كل قسم على حدة | |
for (let i = 0; i < segments.length; i++) { | |
updateLoadingProgress(`جاري تحليل القسم ${i+1} من ${segments.length}...`, | |
`${Math.round(50 + ((i + 1) / segments.length) * 40)}%`); | |
// استدعاء دالة تحليل القسم | |
const analysisResult = await analyzeAlignedPair(segments[i].source, segments[i].target, i+1); | |
// تحديث المسودة بنتائج التحليل | |
updateSegmentAnalysis(i, analysisResult); | |
// تحديث متغير تخزين التحليل | |
analysisSegments[i].analysis = analysisResult.analysis; | |
analysisSegments[i].errors = analysisResult.errors; | |
// تجميع إجمالي الأخطاء | |
totalNumberErrors += analysisResult.errors.numbers; | |
totalMissingErrors += analysisResult.errors.missing; | |
totalMeaningErrors += analysisResult.errors.meaning; | |
// السماح بوقت للمعالجة بين الطلبات | |
await new Promise(resolve => setTimeout(resolve, 200)); | |
} | |
updateLoadingProgress('جاري تحضير النتائج النهائية...', '95%'); | |
// إعداد الملخص النهائي | |
const totalErrors = totalNumberErrors + totalMissingErrors + totalMeaningErrors; | |
if (totalErrors === 0) { | |
addError('لم يتم العثور على أخطاء - النصوص متطابقة', 'info'); | |
} else { | |
addError(`تم الانتهاء من التحليل. العثور على ${totalErrors} خطأ:`, 'info'); | |
if (totalNumberErrors > 0) addError(`- ${totalNumberErrors} اختلاف في الأرقام`, 'warning'); | |
if (totalMissingErrors > 0) addError(`- ${totalMissingErrors} نص مفقود`, 'warning'); | |
if (totalMeaningErrors > 0) addError(`- ${totalMeaningErrors} اختلاف في المعنى`, 'warning'); | |
} | |
// تجهيز العرض المقسم | |
displaySegmentedView(); | |
// تجهيز العرض التفاعلي | |
setupInteractiveView(); | |
// تحديث خريطة الحرارة | |
updateHeatMap(); | |
updateLoadingProgress('تمت العملية بنجاح!', '100%'); | |
setTimeout(() => { | |
hideLoadingIndicator(); | |
}, 1000); | |
} catch (error) { | |
console.error('Error during analysis:', error); | |
addError('حدث خطأ أثناء التحليل: ' + error.message, 'error'); | |
hideLoadingIndicator(); | |
} | |
}); | |
/* ===================================== | |
دالة إضافة التمييز المبدئي للنص | |
===================================== */ | |
function addPreliminaryHighlights(sourceText, targetText) { | |
let highlightedText = sourceText; | |
// 1. تحديد الأرقام المحتملة | |
const numberRegex = /(\d+|[٠١٢٣٤٥٦٧٨٩]+)/g; | |
highlightedText = highlightedText.replace(numberRegex, '<span class="preliminary-highlight-number">\$1</span>'); | |
// 2. تحديد النصوص المفقودة المحتملة | |
const paragraphs = sourceText.split(/\n\s*\n/).filter(p => p.trim().length > 20); | |
for (const paragraph of paragraphs) { | |
if (!isTextSubstantiallyIncluded(paragraph, targetText)) { | |
highlightedText = highlightedText.replace( | |
paragraph, | |
`<span class="preliminary-highlight-missing">${paragraph}</span>` | |
); | |
} | |
} | |
return highlightedText; | |
} | |
/* ===================================== | |
التحقق من تضمين النص بشكل جوهري | |
===================================== */ | |
function isTextSubstantiallyIncluded(text, targetText) { | |
// استخراج الكلمات المهمة (أطول من 3 أحرف) | |
const words = text.split(/\s+/).filter(w => w.length > 3); | |
if (words.length === 0) return true; | |
// حساب عدد الكلمات المهمة الموجودة في النص الهدف | |
const foundCount = words.filter(word => targetText.includes(word)).length; | |
// اعتبار النص مضمن إذا وجدنا أكثر من 50% من الكلمات المهمة | |
return foundCount / words.length > 0.5; | |
} | |
/* ===================================== | |
دالة تقسيم النصوص مع عدد أقصى 6 أقسام - محسنة غير متزامنة | |
===================================== */ | |
async function alignTextsWithModel(sourceText, targetText) { | |
try { | |
// إظهار حالة التقسيم | |
document.getElementById('processingStatus').classList.remove('hidden'); | |
document.getElementById('statusText').textContent = 'جاري تقسيم ومزامنة النصوص...'; | |
document.getElementById('progressBar').style.width = '30%'; | |
updateLoadingProgress('جاري تقسيم ومزامنة النصوص...', '35%'); | |
// استدعاء نموذج DeepSeek للتقسيم والمزامنة | |
const prompt = `مهمتك هي تقسيم النصين التاليين (النص المصدر والنص الهدف) إلى أقسام متوازية بحيث يتطابق كل قسم في المصدر مع ما يقابله في الهدف. | |
ملاحظات مهمة: | |
1. عدد الأقسام يجب أن لا يتجاوز 6 أقسام بأي حال | |
2. قسّم النص إلى أجزاء منطقية ومترابطة | |
3. حاول الحفاظ على تكامل الفقرات والجمل | |
أعِد النتائج بصيغة JSON بهذا الشكل: | |
{ | |
"segments": [ | |
{ | |
"source": "نص المصدر للقسم الأول", | |
"target": "نص الهدف المقابل للقسم الأول" | |
}, | |
... | |
] | |
} | |
النص المصدر: | |
${sourceText} | |
النص الهدف: | |
${targetText}`; | |
const payload = { | |
model: "deepseek-chat", | |
messages: [ | |
{ role: "system", content: "أنت خبير في تقسيم النصوص ومزامنتها للمقارنة والتحليل." }, | |
{ role: "user", content: prompt } | |
], | |
temperature: 0.3, | |
max_tokens: 8000 | |
}; | |
document.getElementById('progressBar').style.width = '50%'; | |
updateLoadingProgress('جاري معالجة تقسيم النصوص...', '40%'); | |
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('فشل استدعاء نموذج DeepSeek: ' + response.statusText); | |
} | |
document.getElementById('progressBar').style.width = '80%'; | |
updateLoadingProgress('جاري تحليل النتائج...', '45%'); | |
const data = await response.json(); | |
const result = data.choices[0].message.content; | |
// استخراج JSON من النتيجة | |
let segments = []; | |
try { | |
const jsonMatch = result.match(/\{[\s\S]*\}/); | |
if (jsonMatch) { | |
const parsedData = JSON.parse(jsonMatch[0]); | |
segments = parsedData.segments || []; | |
} | |
} catch (e) { | |
console.error('خطأ في تحليل نتيجة تقسيم النصوص:', e); | |
// استخدام طريقة بديلة إذا فشل التحليل | |
segments = createFallbackSegments(sourceText, targetText); | |
} | |
// لضمان عدم تخطي الحد الأقصى 6 أقسام | |
if (segments.length > 6) { | |
segments = consolidateSegments(segments, 6); | |
} | |
document.getElementById('progressBar').style.width = '100%'; | |
document.getElementById('statusText').textContent = 'تم تقسيم النصوص بنجاح!'; | |
updateLoadingProgress('تم تقسيم النصوص بنجاح!', '50%'); | |
// إخفاء حالة المعالجة بعد فترة قصيرة | |
setTimeout(() => { | |
document.getElementById('processingStatus').classList.add('hidden'); | |
}, 1000); | |
return segments; | |
} catch (error) { | |
console.error('خطأ في تقسيم النصوص:', error); | |
// إخفاء حالة المعالجة وإظهار رسالة الخطأ | |
document.getElementById('processingStatus').classList.add('hidden'); | |
addError('حدث خطأ أثناء تقسيم النصوص: ' + error.message); | |
// إعادة تقسيم بسيط كحل احتياطي مع تحديد أقصى 6 أقسام | |
return createFallbackSegments(sourceText, targetText); | |
} | |
} | |
/* ===================================== | |
دمج الأقسام لتقليل عددها إلى الحد الأقصى | |
===================================== */ | |
function consolidateSegments(segments, maxSegments) { | |
if (segments.length <= maxSegments) { | |
return segments; | |
} | |
// عدد الأقسام في كل مجموعة جديدة | |
const segmentsPerGroup = Math.ceil(segments.length / maxSegments); | |
const consolidatedSegments = []; | |
for (let i = 0; i < segments.length; i += segmentsPerGroup) { | |
// دمج مجموعة من الأقسام معًا | |
const groupSegments = segments.slice(i, Math.min(i + segmentsPerGroup, segments.length)); | |
const consolidatedSource = groupSegments.map(s => s.source).join('\n\n'); | |
const consolidatedTarget = groupSegments.map(s => s.target).join('\n\n'); | |
consolidatedSegments.push({ | |
source: consolidatedSource, | |
target: consolidatedTarget | |
}); | |
} | |
return consolidatedSegments; | |
} | |
/* ===================================== | |
إنشاء تقسيمات احتياطية - محدودة بـ 6 أقسام | |
===================================== */ | |
function createFallbackSegments(sourceText, targetText) { | |
addError('تم الانتقال إلى آلية التقسيم الاحتياطية', 'warning'); | |
// تقسيم النصوص إلى فقرات | |
const sourceParagraphs = sourceText.split(/\n\s*\n/).filter(p => p.trim()); | |
const targetParagraphs = targetText.split(/\n\s*\n/).filter(p => p.trim()); | |
// حساب عدد الأقسام المطلوبة (6 كحد أقصى) | |
const MAX_SEGMENTS = 6; | |
const sourceSegmentsCount = Math.min(MAX_SEGMENTS, sourceParagraphs.length); | |
const segmentsPerGroup = Math.ceil(sourceParagraphs.length / sourceSegmentsCount); | |
const segments = []; | |
// توزيع فقرات المصدر على الأقسام | |
for (let i = 0; i < sourceSegmentsCount; i++) { | |
const startIdx = i * segmentsPerGroup; | |
const endIdx = Math.min(startIdx + segmentsPerGroup, sourceParagraphs.length); | |
const sourceSegment = sourceParagraphs.slice(startIdx, endIdx).join('\n\n'); | |
// محاولة العثور على أقسام مقابلة في الهدف | |
let targetSegment = ''; | |
// تخمين الأقسام المقابلة في الهدف بناءً على الطول النسبي | |
const targetStartIdx = Math.floor((startIdx / sourceParagraphs.length) * targetParagraphs.length); | |
const targetEndIdx = Math.min(Math.floor((endIdx / sourceParagraphs.length) * targetParagraphs.length), targetParagraphs.length); | |
targetSegment = targetParagraphs.slice(targetStartIdx, targetEndIdx).join('\n\n'); | |
// إذا لم نجد قسمًا مقابلًا، نضع رسالة توضيحية | |
if (!targetSegment) { | |
targetSegment = '(لا يوجد نص مقابل في الهدف)'; | |
} | |
segments.push({ | |
source: sourceSegment, | |
target: targetSegment | |
}); | |
} | |
return segments; | |
} | |
/* ===================================== | |
تحليل قسم واحد باستخدام نموذج الذكاء الاصطناعي - محسنة غير متزامنة | |
===================================== */ | |
async function analyzeAlignedPair(sourceText, targetText, pairNumber) { | |
try { | |
// إظهار حالة التحليل | |
document.getElementById('statusText').textContent = `جاري تحليل القسم ${pairNumber}...`; | |
updateLoadingProgress(`جاري تحليل القسم ${pairNumber}...`, | |
`${Math.round(50 + ((pairNumber) / analysisSegments.length) * 40)}%`); | |
// برومبت محسن للتحليل | |
const prompt = `قارن النص المصدر والنص الهدف التاليين، وحدد بدقة: | |
1. اختلافات الأرقام: ضع الرقم المختلف بين علامتي < > مع الكلمة السابقة والكلمة اللاحقة أو الجملة كاملة | |
2. النصوص المفقودة: ضع النص المفقود في الترجمة بين علامتي __ __ | |
3. اختلافات المعنى: ضع النص الذي تغير معناه بين علامتي [MEANING] [/MEANING] | |
اعتبر الأرقام بمختلف أنظمتها (العربية والهندية والإنجليزية) متطابقة إذا كانت تمثل نفس الرقم. | |
الجمل عادةً تكون من 20 إلى 300 كلمة. النصوص عبارة عن نص سورس ونص تاجرت واحد أصلي والثاني ترجمته. | |
استخراج النصوص المفقودة حيث يوجد في المصدر جملة أو فقرة أو كلمة (بالعربي أو الإنجليزي) وفي النص المقابل لا يوجد ما يعادلها في المعنى بأي لغة. | |
قم بتحليل كل جملة على حدة وتحديد الاختلافات بدقة. | |
لكل اختلاف، قدم شرحاً موجزاً للخطأ والتصحيح المقترح. | |
النص المصدر: | |
${sourceText} | |
النص الهدف: | |
${targetText} | |
`; | |
const payload = { | |
model: "deepseek-chat", | |
messages: [ | |
{ role: "system", content: "أنت خبير لغوي في تحليل ومقارنة النصوص المترجمة بدقة عالية." }, | |
{ role: "user", content: prompt } | |
], | |
temperature: 0.2, | |
max_tokens: 2048 | |
}; | |
// استدعاء API | |
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('فشل استدعاء API للتحليل: ' + response.statusText); | |
} | |
const data = await response.json(); | |
const analysisResult = data.choices[0].message.content.trim(); | |
// تحليل الاختلافات وتصنيفها | |
const numberDiffs = (analysisResult.match(/<[^<>]+>/g) || []).length; | |
const missingTexts = (analysisResult.match(/__(.*?)__/g) || []).length; | |
const meaningDiffs = (analysisResult.match(/\[MEANING\](.*?)\[\/MEANING\]/g) || []).length; | |
const totalDiffs = numberDiffs + missingTexts + meaningDiffs; | |
// إنشاء ملخص للتحليل | |
let summary; | |
if (totalDiffs === 0) { | |
summary = '<span class="perfect-match">لم يتم العثور على اختلافات. النصوص متطابقة.</span>'; | |
} else { | |
summary = `تم العثور على `; | |
const parts = []; | |
if (numberDiffs > 0) parts.push(`<span class="highlight-number">${numberDiffs} اختلاف في الأرقام</span>`); | |
if (missingTexts > 0) parts.push(`<span class="highlight-missing">${missingTexts} نص مفقود</span>`); | |
if (meaningDiffs > 0) parts.push(`<span class="highlight-meaning">${meaningDiffs} اختلاف في المعنى</span>`); | |
summary += parts.join('، ') + "."; | |
} | |
return { | |
pairId: pairNumber, | |
sourceText: sourceText, | |
targetText: targetText, | |
analysis: analysisResult, | |
summary: summary, | |
errors: { | |
numbers: numberDiffs, | |
missing: missingTexts, | |
meaning: meaningDiffs | |
} | |
}; | |
} catch (error) { | |
console.error(`خطأ في تحليل القسم ${pairNumber}:`, error); | |
return { | |
pairId: pairNumber, | |
sourceText: sourceText, | |
targetText: targetText, | |
analysis: "حدث خطأ في التحليل: " + error.message, | |
summary: "فشل التحليل", | |
errors: { numbers: 0, missing: 0, meaning: 0 } | |
}; | |
} | |
} | |
/* ===================================== | |
عرض الأقسام في المسودة - محسن مع تمييز مبدئي | |
===================================== */ | |
function displayDraftSegments(segments) { | |
const container = document.getElementById('paragraphDivisionsContainer'); | |
container.innerHTML = ''; // مسح المحتوى الحالي | |
segments.forEach((segment, index) => { | |
// إنشاء عنصر القسم القابل للطي | |
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>القسم ${index+1}</span> | |
<div class="reanalyze-btn" title="إعادة تحليل هذا القسم فقط" data-segment-index="${index}"> | |
<i class="fas fa-sync-alt"></i> | |
</div> | |
</div> | |
<i class="fas fa-chevron-down"></i> | |
`; | |
// إضافة تمييز مبدئي للنص المصدر | |
const preliminaryHighlightedSource = addPreliminaryHighlights(segment.source, segment.target); | |
// إنشاء محتوى القسم بطريقة محسنة | |
const contentDiv = document.createElement('div'); | |
contentDiv.className = 'section-content'; | |
// استخدام تنسيق محسن للفقرات المتوازية مع التمييز المبدئي | |
contentDiv.innerHTML = ` | |
<div class="aligned-paragraphs mb-4"> | |
<div class="paragraph-ar">${preliminaryHighlightedSource}</div> | |
<div class="paragraph-en">${segment.target}</div> | |
</div> | |
<div id="analysis-segment-${index}" class="paragraph-section"> | |
<h4 class="font-bold text-gray-700 mb-2">التحليل:</h4> | |
<div class="bg-gray-50 p-3 rounded-lg flex items-center justify-center"> | |
<div class="animate-spin h-5 w-5 border-4 border-blue-600 rounded-full border-t-transparent ml-2"></div> | |
<span>جارٍ التحليل...</span> | |
</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'); | |
}); | |
}); | |
// إظهار قسم المسودة | |
document.getElementById('fullTextDraftSection').classList.remove('hidden'); | |
} | |
/* ===================================== | |
تحديث تحليل القسم في المسودة - محسن | |
===================================== */ | |
function updateSegmentAnalysis(segmentIndex, analysisResult) { | |
const analysisContainer = document.getElementById(`analysis-segment-${segmentIndex}`); | |
if (analysisContainer) { | |
// تحسين طريقة عرض نتائج التحليل | |
analysisContainer.innerHTML = ` | |
<h4 class="font-bold text-gray-700 mb-2">التحليل:</h4> | |
<div class="bg-gray-50 p-3 rounded-lg"> | |
${formatAnalysisText(analysisResult.analysis)} | |
</div> | |
<div class="mt-3 p-3 rounded-lg bg-blue-50 border-r-4 border-blue-400"> | |
<span class="font-bold block mb-1">الملخص:</span> ${analysisResult.summary} | |
</div> | |
<div class="mt-3 grid grid-cols-3 gap-2 text-sm"> | |
<div class="bg-yellow-50 p-2 rounded-lg border border-yellow-200 flex flex-col items-center"> | |
<span class="font-bold">اختلافات الأرقام</span> | |
<span class="text-xl font-bold mt-1 ${analysisResult.errors.numbers > 0 ? 'text-yellow-600' : 'text-green-600'}"> | |
${analysisResult.errors.numbers} | |
</span> | |
</div> | |
<div class="bg-blue-50 p-2 rounded-lg border border-blue-200 flex flex-col items-center"> | |
<span class="font-bold">نصوص مفقودة</span> | |
<span class="text-xl font-bold mt-1 ${analysisResult.errors.missing > 0 ? 'text-blue-600' : 'text-green-600'}"> | |
${analysisResult.errors.missing} | |
</span> | |
</div> | |
<div class="bg-red-50 p-2 rounded-lg border border-red-200 flex flex-col items-center"> | |
<span class="font-bold">اختلافات المعنى</span> | |
<span class="text-xl font-bold mt-1 ${analysisResult.errors.meaning > 0 ? 'text-red-600' : 'text-green-600'}"> | |
${analysisResult.errors.meaning} | |
</span> | |
</div> | |
</div> | |
`; | |
} | |
} | |
/* ===================================== | |
دالة تمييز النص - محسنة لتمييز الجمل الكاملة | |
===================================== */ | |
function applyHighlights(originalText, targetText, analysisOutput) { | |
// تقسيم النص الأصلي إلى جمل | |
const sentences = splitIntoSentences(originalText); | |
// تطبيق تمييز للنص المفقود تماما والمفقود جزئيا | |
const enhancedSourceText = highlightMissingText(originalText, targetText); | |
// استخراج النصوص التي بها مشاكل من التحليل | |
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 highlightedText = sentences.map((sentence, index) => { | |
let hasError = false; | |
let highlightedSentence = sentence; | |
let errorType = ''; | |
// تحديد اختلافات الأرقام | |
for (const phrase of numberMatches) { | |
if (sentence.includes(phrase)) { | |
const regex = new RegExp(escapeRegExp(phrase), 'g'); | |
highlightedSentence = highlightedSentence.replace(regex, `<span class="highlight-number" data-error-type="number" data-explanation="تأكد من صحة الرقم في النص الهدف وتطابقه مع النص المصدر">${phrase}</span>`); | |
hasError = true; | |
errorType = 'number'; | |
} | |
} | |
// تحديد النصوص المفقودة | |
for (const phrase of missingMatches) { | |
if (sentence.includes(phrase)) { | |
const regex = new RegExp(escapeRegExp(phrase), 'g'); | |
highlightedSentence = highlightedSentence.replace(regex, `<span class="highlight-missing" data-error-type="missing" data-explanation="أضف النص المفقود إلى الترجمة للحفاظ على اكتمال المعنى">${phrase}</span>`); | |
hasError = true; | |
errorType = errorType || 'missing'; | |
} | |
} | |
// تحديد اختلافات المعنى | |
for (const phrase of meaningMatches) { | |
if (sentence.includes(phrase)) { | |
const regex = new RegExp(escapeRegExp(phrase), 'g'); | |
highlightedSentence = highlightedSentence.replace(regex, `<span class="highlight-meaning" data-error-type="meaning" data-explanation="راجع الترجمة للتأكد من نقل المعنى الصحيح دون تحريف">${phrase}</span>`); | |
hasError = true; | |
errorType = errorType || 'meaning'; | |
} | |
} | |
// إذا كان هناك خطأ، قم بتمييز الجملة كاملة | |
if (hasError) { | |
return `<span class="sentence-with-error" data-error-id="${index}" data-error-type="${errorType}" data-sentence-number="${index+1}">${highlightedSentence}</span>`; | |
} | |
return highlightedSentence; | |
}).join(' '); | |
return highlightedText; | |
} | |
/* ===================================== | |
دالة تمييز النص المفقود | |
===================================== */ | |
function highlightMissingText(sourceText, targetText) { | |
// تقسيم النصوص إلى فقرات | |
const sourceParagraphs = sourceText.split(/\n\s*\n/).filter(p => p.trim()); | |
const targetParagraphs = targetText.split(/\n\s*\n/).filter(p => p.trim()); | |
// مصفوفة لتخزين النص المعدل | |
let modifiedSource = sourceText; | |
// البحث عن الفقرات المفقودة تماما في الهدف | |
for (const paragraph of sourceParagraphs) { | |
if (paragraph.trim().length < 10) continue; // تجاهل الفقرات القصيرة جدا | |
// فحص ما إذا كانت الفقرة مفقودة تماما | |
let isCompletelyMissing = true; | |
// البحث عن أجزاء من الفقرة في النص الهدف | |
for (const targetPara of targetParagraphs) { | |
if (targetPara.includes(paragraph) || paragraph.includes(targetPara)) { | |
isCompletelyMissing = false; | |
break; | |
} | |
} | |
// إذا كانت مفقودة تماما، نميزها بشكل مختلف | |
if (isCompletelyMissing) { | |
modifiedSource = modifiedSource.replace( | |
paragraph, | |
`<span class="completely-missing" title="هذا النص مفقود تماما في الترجمة">${paragraph}</span>` | |
); | |
} | |
} | |
// البحث عن الجمل المفقودة جزئيا | |
const sourceLines = sourceParagraphs.join('\n').split(/[.!?]\s+/); | |
for (const line of sourceLines) { | |
if (line.trim().length < 5) continue; // تجاهل الجمل القصيرة جدا | |
// البحث عن جمل موجودة في السورس ومفقودة في التارجت | |
let isPartiallyMissing = true; | |
if (targetText.includes(line)) { | |
isPartiallyMissing = false; | |
} else { | |
// البحث عن كلمات رئيسية من الجملة في التارجت | |
const keywords = line.split(/\s+/).filter(word => word.length > 3); | |
let foundKeywords = 0; | |
for (const keyword of keywords) { | |
if (targetText.includes(keyword)) { | |
foundKeywords++; | |
} | |
} | |
// إذا وجدنا أكثر من 50% من الكلمات الرئيسية، فهي مفقودة جزئيا | |
if (keywords.length > 0 && foundKeywords / keywords.length > 0.5) { | |
isPartiallyMissing = false; | |
} | |
} | |
// تمييز الجمل المفقودة جزئيا فقط إذا لم تكن مميزة بالفعل كمفقودة تماما | |
if (isPartiallyMissing && !modifiedSource.includes(`<span class="completely-missing"`)) { | |
modifiedSource = modifiedSource.replace( | |
line, | |
`<span class="partially-missing" title="هذا النص موجود بشكل جزئي في الترجمة">${line}</span>` | |
); | |
} | |
} | |
return modifiedSource; | |
} | |
/* ===================================== | |
عرض النتائج في العرض المقسم - محسن | |
===================================== */ | |
function displaySegmentedView() { | |
const container = document.getElementById('segmentedComparisonContainer'); | |
container.innerHTML = ''; // مسح المحتوى الحالي | |
// إضافة مقطع لكل قسم | |
analysisSegments.forEach((segment, index) => { | |
// تحديد أنواع الأخطاء في هذا القسم | |
const hasNumbers = segment.errors.numbers > 0; | |
const hasMissing = segment.errors.missing > 0; | |
const hasMeaning = segment.errors.meaning > 0; | |
// إنشاء علامات الأخطاء | |
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 tag-success">مطابق</span>'; | |
} | |
// إنشاء عنصر المقطع | |
const segmentDiv = document.createElement('div'); | |
segmentDiv.className = 'segment-comparison'; | |
segmentDiv.dataset.segmentIndex = index; | |
// تطبيق التحديد على نصوص المقطع | |
const sourceHighlighted = applyHighlights(segment.source, segment.target, segment.analysis); | |
const targetHighlighted = applyHighlights(segment.target, segment.source, segment.analysis); | |
// إعداد HTML للمقطع | |
segmentDiv.innerHTML = ` | |
<div class="segment-header"> | |
<div>المقطع ${index+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"> | |
${formatAnalysisText(segment.analysis)} | |
</div> | |
`; | |
container.appendChild(segmentDiv); | |
}); | |
// إضافة شرح للعرض المقسم | |
document.getElementById('segmentViewExplanation').innerHTML = generateSegmentViewExplanation(); | |
// إضافة مستمعي الأحداث للتحديدات | |
setTimeout(() => { | |
document.querySelectorAll('.segment-source .sentence-with-error').forEach(element => { | |
element.addEventListener('click', function() { | |
// الحصول على النص الكامل للجملة | |
const sentenceText = this.textContent; | |
// تحديد نوع الخطأ من خلال الفئات داخل الجملة | |
let errorType = 'general'; | |
let errorSpecificText = ''; | |
// البحث عن الخطأ المحدد داخل الجملة | |
const numberHighlight = this.querySelector('.highlight-number'); | |
const missingHighlight = this.querySelector('.highlight-missing'); | |
const meaningHighlight = this.querySelector('.highlight-meaning'); | |
if (numberHighlight) { | |
errorType = 'number'; | |
errorSpecificText = numberHighlight.textContent; | |
} else if (missingHighlight) { | |
errorType = 'missing'; | |
errorSpecificText = missingHighlight.textContent; | |
} else if (meaningHighlight) { | |
errorType = 'meaning'; | |
errorSpecificText = meaningHighlight.textContent; | |
} | |
const sentenceNumber = this.getAttribute('data-sentence-number'); | |
// إعداد أمثلة حسب نوع الخطأ | |
let examples = null; | |
if (errorType === 'number') { | |
examples = { | |
incorrect: `المادة <span class="highlight-number">٥</span> من القانون، تُطبق الغرامة المقدرة <span class="highlight-number">٤٥٠</span> دينارًا.`, | |
correct: `المادة <span class="corrected-text">٥</span> من القانون، تُطبق الغرامة المقدرة <span class="corrected-text">٤٥٠</span> دينارًا.` | |
}; | |
} else if (errorType === 'missing') { | |
examples = { | |
incorrect: `اتفق الطرفان على أن يتم تسليم البضائع <span class="highlight-missing">خلال 30 يومًا من توقيع العقد</span>.`, | |
correct: `اتفق الطرفان على أن يتم تسليم البضائع <span class="corrected-text">خلال 30 يومًا من توقيع العقد</span>.` | |
}; | |
} else if (errorType === 'meaning') { | |
examples = { | |
incorrect: `أقرت المحكمة <span class="highlight-meaning">بإدانة</span> المتهم.`, | |
correct: `أقرت المحكمة <span class="corrected-text">ببراءة</span> المتهم.` | |
}; | |
} | |
// إعداد شرح أكثر ودية مع أمثلة | |
let explanation = ''; | |
if (errorType === 'number') { | |
explanation = `يبدو أن هناك اختلافًا في الأرقام في هذه الجملة. الرقم "${errorSpecificText}" في النص المصدر لا يتطابق مع النص الهدف. قد يكون هناك خطأ في نقل الرقم أو تحويله بين أنظمة الأرقام المختلفة (العربية، الإنجليزية، الهندية).`; | |
} else if (errorType === 'missing') { | |
explanation = `هناك نص مفقود في الترجمة! النص "${errorSpecificText}" موجود في المصدر ولكن لم يتم ترجمته أو تضمينه في النص الهدف. هذا يؤدي إلى فقدان جزء من المعنى الأصلي.`; | |
} else if (errorType === 'meaning') { | |
explanation = `هناك اختلاف مهم في المعنى! النص "${errorSpecificText}" تمت ترجمته بطريقة تغير المعنى الأصلي. قد يكون هناك خطأ في فهم السياق أو اختيار المصطلحات المناسبة.`; | |
} else { | |
explanation = `هناك خطأ في هذه الجملة: "${sentenceText}"<br><br>يرجى مراجعة الترجمة للتأكد من دقة المعنى والتطابق بين النصين.`; | |
} | |
// استخدام النافذة المنبثقة المحسنة | |
showEnhancedPopup(errorType, sentenceText, explanation, examples); | |
}); | |
}); | |
}, 100); | |
} | |
/* ===================================== | |
إعداد وعرض العرض التفاعلي - محسن | |
===================================== */ | |
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], 15) | |
}); | |
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], 15) | |
}); | |
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], 15) | |
}); | |
meaningDiffCount++; | |
}); | |
}); | |
// تحديث عدادات الاختلافات | |
document.getElementById('numberDiffCount').textContent = numberDiffCount; | |
document.getElementById('missingTextCount').textContent = missingTextCount; | |
document.getElementById('meaningDiffCount').textContent = meaningDiffCount; | |
// إعداد توصيات المعالجة | |
const recommendationsContainer = document.getElementById('diffRecommendations'); | |
recommendationsContainer.innerHTML = ''; | |
if (allDifferences.length === 0) { | |
recommendationsContainer.innerHTML = ` | |
<div class="flex items-center justify-center p-4"> | |
<div class="text-center"> | |
<i class="fas fa-check-circle text-green-500 text-4xl mb-2"></i> | |
<p>لا توجد اختلافات تحتاج إلى معالجة!</p> | |
</div> | |
</div>`; | |
} else { | |
// إضافة توصيات مخصصة بناءً على أنواع الاختلافات | |
if (numberDiffCount > 0) { | |
recommendationsContainer.innerHTML += `<p class="flex items-center"><i class="fas fa-hashtag text-yellow-500 ml-2"></i> راجع الأرقام واحرص على تطابقها بين النصين.</p>`; | |
} | |
if (missingTextCount > 0) { | |
recommendationsContainer.innerHTML += `<p class="flex items-center"><i class="fas fa-plus-circle text-blue-500 ml-2"></i> أضف النصوص المفقودة في الترجمة لضمان اكتمال المحتوى.</p>`; | |
} | |
if (meaningDiffCount > 0) { | |
recommendationsContainer.innerHTML += `<p class="flex items-center"><i class="fas fa-exchange-alt text-red-500 ml-2"></i> صحح اختلافات المعنى لضمان دقة الترجمة.</p>`; | |
} | |
// توصية عامة | |
recommendationsContainer.innerHTML += `<p class="flex items-center mt-2 pt-2 border-t border-green-200"><i class="fas fa-th-large text-green-500 ml-2"></i> استخدم وضع العرض المقسم للتعديل الدقيق.</p>`; | |
} | |
// تعيين أحداث النقر لأزرار التنقل | |
document.getElementById('prevDiff').addEventListener('click', showPreviousDifference); | |
document.getElementById('nextDiff').addEventListener('click', showNextDifference); | |
// إعداد الاختلاف الأول | |
if (allDifferences.length > 0) { | |
currentDiffIndex = 0; | |
document.getElementById('prevDiff').disabled = false; | |
document.getElementById('nextDiff').disabled = false; | |
displayCurrentDifference(); | |
} else { | |
document.getElementById('diffCounter').textContent = "0/0"; | |
document.getElementById('currentDiffDisplay').innerHTML = ` | |
<div class="flex flex-col items-center justify-center py-8"> | |
<div class="text-6xl text-green-300 mb-4"> | |
<i class="fas fa-check-circle"></i> | |
</div> | |
<p class="text-gray-500 text-center">لم يتم العثور على اختلافات</p> | |
<p class="text-green-500 text-center mt-2 font-bold">النصوص متطابقة!</p> | |
</div>`; | |
document.getElementById('prevDiff').disabled = true; | |
document.getElementById('nextDiff').disabled = true; | |
} | |
// إضافة توضيحات للعرض التفاعلي | |
document.getElementById('interactiveViewExplanation').innerHTML = generateInteractiveViewExplanation(); | |
} | |
/* ===================================== | |
دوال مساعدة للعرض التفاعلي | |
===================================== */ | |
// الحصول على السياق المحيط بنص معين | |
function getContextAroundMatch(text, match, contextSize) { | |
const index = text.indexOf(match); | |
if (index === -1) return ""; | |
// الحصول على جملة كاملة تحتوي على النص المطابق | |
const start = Math.max(0, text.lastIndexOf('.', index) + 1); | |
const end = Math.min(text.length, text.indexOf('.', index + match.length) + 1); | |
// إذا لم نتمكن من العثور على جملة كاملة، نستخدم عدد الأحرف | |
let context; | |
if (end - start < 10) { | |
const altStart = Math.max(0, index - contextSize); | |
const altEnd = Math.min(text.length, index + match.length + contextSize); | |
context = text.substring(altStart, altEnd); | |
} else { | |
context = text.substring(start, end); | |
} | |
// تمييز النص المطابق | |
const highlightedContext = context.replace( | |
new RegExp(`(${escapeRegExp(match)})`, 'g'), | |
`<span class="font-bold text-blue-700 bg-blue-100 px-1 rounded">\$1</span>` | |
); | |
return highlightedContext; | |
} | |
// عرض الاختلاف السابق | |
function showPreviousDifference() { | |
if (allDifferences.length === 0) return; | |
currentDiffIndex = (currentDiffIndex - 1 + allDifferences.length) % allDifferences.length; | |
displayCurrentDifference(); | |
} | |
// عرض الاختلاف التالي | |
function showNextDifference() { | |
if (allDifferences.length === 0) return; | |
currentDiffIndex = (currentDiffIndex + 1) % allDifferences.length; | |
displayCurrentDifference(); | |
} | |
// عرض الاختلاف الحالي | |
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"; | |
document.getElementById('diffDetailedView').classList.add('hidden'); | |
document.getElementById('diffReference').classList.add('hidden'); | |
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; | |
// إضافة رقم المقطع ونوع الخطأ وعرض السياق | |
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 flex items-center"> | |
<i class="fas fa-quote-right text-gray-400 ml-2"></i> | |
<span>السياق:</span> | |
</div> | |
<div class="bg-gray-50 p-4 rounded-lg mb-3 leading-relaxed">${highlightedContext}</div> | |
<div class="text-sm text-gray-600"> | |
<div class="font-bold mb-1 flex items-center"> | |
<i class="fas fa-lightbulb text-yellow-500 ml-2"></i> | |
<span>التوصية:</span> | |
</div> | |
<div class="p-2 bg-yellow-50 rounded-lg border-r-3 border-yellow-400"> | |
${getRecommendationForDiff(diff)} | |
</div> | |
</div> | |
`; | |
document.getElementById('diffCounter').textContent = `${currentDiffIndex + 1}/${allDifferences.length}`; | |
// عرض النص في المصدر والهدف | |
document.getElementById('diffDetailedView').classList.remove('hidden'); | |
// عرض النص المصدر | |
document.getElementById('diffSourceText').innerHTML = ` | |
<div class="p-2 bg-white rounded-lg border border-gray-200 leading-relaxed"> | |
${highlightSourceText(segment.source, diff.text)} | |
</div>`; | |
// عرض النص الهدف | |
document.getElementById('diffTargetText').innerHTML = ` | |
<div class="p-2 bg-white rounded-lg border border-gray-200 leading-relaxed"> | |
${highlightTargetText(segment.target, diff)} | |
</div>`; | |
// عرض قسم المراجع إذا كان متاحا | |
if (diff.reference) { | |
document.getElementById('diffReference').classList.remove('hidden'); | |
document.getElementById('diffReferenceText').textContent = diff.reference; | |
} else { | |
document.getElementById('diffReference').classList.add('hidden'); | |
} | |
} | |
// تمييز النص في المصدر | |
function highlightSourceText(sourceText, diffText) { | |
return sourceText.replace( | |
new RegExp(`(${escapeRegExp(diffText)})`, 'g'), | |
`<mark class="bg-yellow-200 px-1 rounded">\$1</mark>` | |
); | |
} | |
// تمييز النص في الهدف | |
function highlightTargetText(targetText, diff) { | |
if (diff.type === 'missing') { | |
// بالنسبة للنص المفقود، نحاول العثور على مكان يجب إدراجه | |
const words = diff.text.split(/\s+/).filter(w => w.length > 3); | |
let markedText = targetText; | |
// البحث عن كلمات قبل وبعد النص المفقود في السياق | |
const contextBefore = diff.context.split(diff.text)[0]?.trim(); | |
const contextAfter = diff.context.split(diff.text)[1]?.trim(); | |
if (contextBefore && targetText.includes(contextBefore)) { | |
const index = targetText.indexOf(contextBefore) + contextBefore.length; | |
markedText = | |
targetText.substring(0, index) + | |
` <mark class="bg-blue-200 px-1 rounded border border-blue-400 border-dashed">[يجب إضافة: ${diff.text}]</mark> ` + | |
targetText.substring(index); | |
} else if (contextAfter && targetText.includes(contextAfter)) { | |
const index = targetText.indexOf(contextAfter); | |
markedText = | |
targetText.substring(0, index) + | |
` <mark class="bg-blue-200 px-1 rounded border border-blue-400 border-dashed">[يجب إضافة: ${diff.text}]</mark> ` + | |
targetText.substring(index); | |
} else { | |
// إذا لم نتمكن من تحديد المكان، نضيف علامة في النهاية | |
markedText += ` <mark class="bg-blue-200 px-1 rounded border border-blue-400 border-dashed">[نص مفقود: ${diff.text}]</mark>`; | |
} | |
return markedText; | |
} | |
else if (diff.type === 'number') { | |
// البحث عن الأرقام في النص الهدف وتحديد الرقم المختلف | |
const numbers = targetText.match(/\d+/g) || []; | |
if (numbers.length === 0) return targetText; | |
let markedText = targetText; | |
// تحديد الرقم الذي يختلف عن النص المصدر | |
for (const num of numbers) { | |
if (num !== diff.text) { | |
markedText = markedText.replace( | |
new RegExp(`(${num})`, 'g'), | |
`<mark class="bg-yellow-200 px-1 rounded">${num}</mark>` | |
); | |
} | |
} | |
return markedText; | |
} | |
else { // meaning | |
// البحث عن عبارات مشابهة في النص الهدف | |
const words = diff.text.split(/\s+/).filter(w => w.length > 3); | |
let markedText = targetText; | |
for (const word of words) { | |
if (targetText.includes(word)) { | |
const regex = new RegExp(`(.{0,10}${escapeRegExp(word)}.{0,10})`, 'g'); | |
markedText = markedText.replace( | |
regex, | |
`<mark class="bg-red-200 px-1 rounded">\$1</mark>` | |
); | |
} | |
} | |
return markedText; | |
} | |
} | |
// الحصول على توصية مخصصة لكل نوع اختلاف - محسنة لتكون أكثر ودية | |
function getRecommendationForDiff(diff) { | |
if (diff.type === 'number') { | |
return 'يا صديقي، يبدو أن هناك خطأ في الرقم. في النص المصدر يظهر الرقم بشكل مختلف عما هو في النص الهدف. تأكد من تطابق الأرقام في النصين واستخدام نفس الصيغة.'; | |
} else if (diff.type === 'missing') { | |
return 'يا صديقي، هناك نص مفقود في الترجمة! تأكد من إضافة هذا النص لأنه موجود في المصدر ويحتوي على معلومات مهمة للمحافظة على اكتمال المعنى.'; | |
} else if (diff.type === 'meaning') { | |
return 'يا صديقي، هناك اختلاف في المعنى بين النصين. النص في المصدر يقول شيئًا مختلفًا عما تمت ترجمته. راجع هذا الجزء للتأكد من دقة الترجمة ونقل المعنى الصحيح.'; | |
} | |
return ''; | |
} | |
/* ===================================== | |
هروب أحرف Regex الخاصة | |
===================================== */ | |
function escapeRegExp(string) { | |
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
} | |
/* ===================================== | |
تنسيق نص التحليل بتمييز الكلمات المهمة | |
===================================== */ | |
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 generateSegmentViewExplanation() { | |
return ` | |
<div class="p-4 bg-blue-50 rounded-lg border border-blue-200"> | |
<h4 class="font-bold text-blue-800 mb-2 flex items-center"> | |
<i class="fas fa-info-circle ml-2"></i> | |
عن العرض المقسم | |
</h4> | |
<p class="text-blue-800"> | |
يقوم هذا العرض بتقسيم النص إلى مقاطع متوازية لتسهيل المقارنة بين النص المصدر والترجمة. | |
انقر على النص المميز للحصول على تفاصيل إضافية حول الاختلاف. | |
</p> | |
</div> | |
<div class="mt-4 p-4 bg-yellow-50 rounded-lg border border-yellow-200"> | |
<h4 class="font-bold text-yellow-800 mb-2 flex items-center"> | |
<i class="fas fa-lightbulb ml-2"></i> | |
نصائح للاستخدام | |
</h4> | |
<ul class="list-disc pr-8 text-sm text-yellow-700 space-y-1"> | |
<li>استخدم فلتر الأخطاء في الأعلى لعرض نوع محدد من الاختلافات</li> | |
<li>انقر على النص المميز للحصول على تفاصيل إضافية</li> | |
<li>قارن المقاطع بشكل مباشر لتحديد المشكلات بدقة</li> | |
</ul> | |
</div>`; | |
} | |
/* ===================================== | |
توليد شرح للعرض التفاعلي | |
===================================== */ | |
function generateInteractiveViewExplanation() { | |
return ` | |
<div class="p-4 bg-blue-50 rounded-lg border border-blue-200"> | |
<h4 class="font-bold text-blue-800 mb-2 flex items-center"> | |
<i class="fas fa-info-circle ml-2"></i> | |
عن العرض التفاعلي | |
</h4> | |
<p class="text-blue-800"> | |
يقوم هذا العرض بتمكينك من استعراض الاختلافات واحدة تلو الأخرى بطريقة تفاعلية. | |
استخدم أزرار "التالي" و"السابق" للتنقل بين الاختلافات المختلفة. | |
</p> | |
</div> | |
<div class="mt-4 p-4 bg-yellow-50 rounded-lg border border-yellow-200"> | |
<h4 class="font-bold text-yellow-800 mb-2 flex items-center"> | |
<i class="fas fa-lightbulb ml-2"></i> | |
مزايا هذا العرض | |
</h4> | |
<ul class="list-disc pr-8 text-sm text-yellow-700 space-y-1"> | |
<li>تركيز أفضل على كل اختلاف على حدة</li> | |
<li>رؤية النص المصدر والهدف معًا لكل اختلاف</li> | |
<li>الحصول على توصيات محددة لمعالجة كل مشكلة</li> | |
</ul> | |
</div>`; | |
} | |
/* ===================================== | |
/* ===================================== | |
دالة للتحقق إذا كان النص مضمن جزئيا | |
===================================== */ | |
function isPartiallyIncluded(text, targetText) { | |
// تقسيم النص إلى كلمات رئيسية | |
const keywords = text.split(/\s+/).filter(word => word.length > 3); | |
let foundKeywords = 0; | |
// حساب عدد الكلمات الرئيسية الموجودة في النص الهدف | |
for (const keyword of keywords) { | |
if (targetText.includes(keyword)) { | |
foundKeywords++; | |
} | |
} | |
// اعتبار النص مضمن جزئيا إذا وجدنا أكثر من 30% من الكلمات | |
return keywords.length > 0 && foundKeywords / keywords.length > 0.3; | |
} | |
/* ===================================== | |
تنزيل التقرير بصيغة Word | |
===================================== */ | |
function downloadWordReport() { | |
// إنشاء محتوى ملف Word | |
let wordContent = ` | |
<!DOCTYPE html> | |
<html xmlns:o='urn:schemas-microsoft-com:office:office' | |
xmlns:w='urn:schemas-microsoft-com:office:word' | |
xmlns='http://www.w3.org/TR/REC-html40'> | |
<head> | |
<meta charset='utf-8'> | |
<title>تقرير تحليل النصوص - شركة الريحان</title> | |
<style> | |
@page { | |
size: 21cm 29.7cm; | |
margin: 2cm; | |
mso-page-orientation: portrait; | |
} | |
body { | |
font-family: 'Arial', sans-serif; | |
direction: rtl; | |
text-align: right; | |
} | |
h1, h2, h3, h4 { | |
color: #3b82f6; | |
} | |
.header { | |
text-align: center; | |
margin-bottom: 20px; | |
border-bottom: 1px solid #ddd; | |
padding-bottom: 10px; | |
} | |
.header h1 { | |
color: #3b82f6; | |
margin-bottom: 5px; | |
} | |
.segment { | |
margin-bottom: 30px; | |
page-break-inside: avoid; | |
} | |
.segment-title { | |
background-color: #f3f4f6; | |
padding: 5px; | |
margin-bottom: 10px; | |
font-weight: bold; | |
border-right: 4px solid #3b82f6; | |
} | |
.source-text, .target-text { | |
margin-bottom: 10px; | |
padding: 10px; | |
border: 1px solid #e5e7eb; | |
} | |
.source-text { | |
background-color: #f0f9ff; | |
} | |
.target-text { | |
background-color: #fdf2f8; | |
} | |
.analysis { | |
margin-top: 10px; | |
background-color: #fffbeb; | |
padding: 10px; | |
border: 1px solid #fcd34d; | |
} | |
.error-numbers { | |
color: #b45309; | |
} | |
.error-missing { | |
color: #1e40af; | |
} | |
.error-meaning { | |
color: #b91c1c; | |
} | |
.footer { | |
text-align: center; | |
margin-top: 20px; | |
font-size: 10px; | |
color: #6b7280; | |
border-top: 1px solid #ddd; | |
padding-top: 10px; | |
} | |
.summary { | |
margin: 20px 0; | |
border: 1px solid #e5e7eb; | |
padding: 10px; | |
} | |
.summary-title { | |
font-weight: bold; | |
margin-bottom: 10px; | |
border-bottom: 1px solid #e5e7eb; | |
padding-bottom: 5px; | |
} | |
table { | |
width: 100%; | |
border-collapse: collapse; | |
margin: 10px 0; | |
} | |
table, th, td { | |
border: 1px solid #e5e7eb; | |
padding: 5px; | |
} | |
th { | |
background-color: #f3f4f6; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<h1>تقرير تحليل النصوص المترجمة</h1> | |
<div>شركة الريحان للترجمة المعتمدة</div> | |
<div>تاريخ التقرير: ${new Date().toLocaleDateString('ar-EG')}</div> | |
${correctionApplied ? '<div style="color: #22c55e; font-weight: bold;">تم تطبيق تصحيحات المستندات الرسمية</div>' : ''} | |
</div> | |
<div class="summary"> | |
<div class="summary-title">ملخص التحليل</div> | |
<table> | |
<tr> | |
<th>نوع الاختلاف</th> | |
<th>العدد</th> | |
</tr>`; | |
// حساب إجمالي الأخطاء | |
let totalNumbers = 0; | |
let totalMissing = 0; | |
let totalMeaning = 0; | |
analysisSegments.forEach(segment => { | |
totalNumbers += segment.errors.numbers; | |
totalMissing += segment.errors.missing; | |
totalMeaning += segment.errors.meaning; | |
}); | |
wordContent += ` | |
<tr> | |
<td>اختلافات الأرقام</td> | |
<td>${totalNumbers}</td> | |
</tr> | |
<tr> | |
<td>النصوص المفقودة</td> | |
<td>${totalMissing}</td> | |
</tr> | |
<tr> | |
<td>اختلافات المعنى</td> | |
<td>${totalMeaning}</td> | |
</tr> | |
<tr> | |
<td><strong>إجمالي الاختلافات</strong></td> | |
<td><strong>${totalNumbers + totalMissing + totalMeaning}</strong></td> | |
</tr> | |
</table> | |
</div>`; | |
// إضافة تفاصيل كل قسم | |
analysisSegments.forEach((segment, index) => { | |
wordContent += ` | |
<div class="segment"> | |
<div class="segment-title">القسم ${index + 1}</div> | |
<div class="source-text"> | |
<strong>النص المصدر:</strong><br> | |
${segment.source} | |
</div> | |
<div class="target-text"> | |
<strong>النص الهدف:</strong><br> | |
${segment.target} | |
</div> | |
<div class="analysis"> | |
<strong>التحليل:</strong><br> | |
${segment.analysis.replace(/<([^<>]+)>/g, '<span class="error-numbers"><$1></span>') | |
.replace(/__(.*?)__/g, '<span class="error-missing">__$1__</span>') | |
.replace(/\[MEANING\](.*?)\[\/MEANING\]/g, '<span class="error-meaning">$1</span>')} | |
</div> | |
</div>`; | |
}); | |
// إضافة التذييل | |
wordContent += ` | |
<div class="footer"> | |
تم إنشاء هذا التقرير تلقائيًا بواسطة نظام المراجع الذكي - جميع الحقوق محفوظة لشركة فاست برو للبرمجيات والذكاء الاصطناعي © ${new Date().getFullYear()} | |
</div> | |
</body> | |
</html>`; | |
// تنزيل الملف | |
const blob = new Blob(['\ufeff', wordContent], { type: 'application/msword' }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = 'تقرير_تحليل_النصوص.doc'; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
URL.revokeObjectURL(url); | |
} | |
/* ===================================== | |
عرض الأخطاء والرسائل - محسنة | |
===================================== */ | |
function addError(message, type = 'error') { | |
const errorsList = document.getElementById('errorsList'); | |
if (!errorsList) return; | |
const errorDiv = document.createElement('div'); | |
// تحسين مظهر رسائل الخطأ | |
let bgColor, borderColor, textColor, icon; | |
if (type === 'error') { | |
bgColor = 'bg-red-50'; | |
borderColor = 'border-red-200'; | |
textColor = 'text-red-700'; | |
icon = 'exclamation-circle text-red-500'; | |
} else if (type === 'info') { | |
bgColor = 'bg-blue-50'; | |
borderColor = 'border-blue-200'; | |
textColor = 'text-blue-700'; | |
icon = 'info-circle text-blue-500'; | |
} else { // warning | |
bgColor = 'bg-yellow-50'; | |
borderColor = 'border-yellow-200'; | |
textColor = 'text-yellow-700'; | |
icon = 'exclamation-triangle text-yellow-500'; | |
} | |
errorDiv.className = `p-4 rounded-xl ${bgColor} ${textColor} border ${borderColor} shadow-sm mb-3 animate-scale`; | |
errorDiv.innerHTML = ` | |
<div class="flex items-center"> | |
<i class="fas fa-${icon} text-xl ml-3"></i> | |
<span class="font-medium">${message}</span> | |
<button class="mr-auto text-gray-400 hover:text-gray-600 transition-colors"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div>`; | |
// إضافة زر إغلاق للرسالة | |
const closeBtn = errorDiv.querySelector('button'); | |
closeBtn.addEventListener('click', () => { | |
errorDiv.style.opacity = '0'; | |
errorDiv.style.transform = 'translateY(-10px)'; | |
errorDiv.style.transition = 'opacity 0.3s, transform 0.3s'; | |
setTimeout(() => { | |
errorsList.removeChild(errorDiv); | |
}, 300); | |
}); | |
errorsList.appendChild(errorDiv); | |
// تمرير تلقائي إلى أحدث رسالة | |
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
} | |
/* ===================================== | |
تطبيق فلتر الأخطاء على جميع طرق العرض | |
===================================== */ | |
function applyErrorFilter(filterType) { | |
// تطبيق الفلتر على العرض الكلاسيكي | |
if (filterType === 'all') { | |
// إظهار كل الأخطاء | |
document.querySelectorAll('.highlight-number, .highlight-missing, .highlight-meaning, .completely-missing, .partially-missing').forEach(el => { | |
el.style.display = ''; | |
}); | |
// إعادة تفعيل جميع الجمل | |
document.querySelectorAll('.sentence-with-error').forEach(el => { | |
el.classList.remove('opacity-50'); | |
}); | |
} else { | |
// إخفاء كل الأخطاء أولاً | |
document.querySelectorAll('.highlight-number, .highlight-missing, .highlight-meaning, .completely-missing, .partially-missing').forEach(el => { | |
el.style.display = 'none'; | |
}); | |
// إضفاء شفافية على جميع الجمل | |
document.querySelectorAll('.sentence-with-error').forEach(el => { | |
el.classList.add('opacity-50'); | |
}); | |
// إظهار الأخطاء المطلوبة فقط | |
let selector = ''; | |
if (filterType === 'number') selector = '.highlight-number'; | |
else if (filterType === 'missing') selector = '.highlight-missing, .completely-missing, .partially-missing'; | |
else if (filterType === 'meaning') selector = '.highlight-meaning'; | |
document.querySelectorAll(selector).forEach(el => { | |
el.style.display = ''; | |
// إزالة الشفافية عن الجمل المتأثرة | |
const errorId = el.getAttribute('data-error-id'); | |
if (errorId) { | |
document.querySelectorAll(`.sentence-with-error[data-error-id="${errorId}"]`).forEach(sentence => { | |
sentence.classList.remove('opacity-50'); | |
}); | |
} | |
}); | |
} | |
// تطبيق الفلتر على العرض المقسم والتفاعلي | |
// (الرمز يشبه العرض الكلاسيكي مع تعديلات بسيطة للتناسب مع هيكل هذه العروض) | |
} | |
</script> | |
</body> | |
</html> |