| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>نظام المقارنة المطور - شركة الريحان للترجمة</title> |
| |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet"> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.6.0/mammoth.browser.min.js"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> |
| <style> |
| |
| |
| |
| @keyframes gradient { |
| 0% { background-position: 0% 50%; } |
| 50% { background-position: 100% 50%; } |
| 100% { background-position: 0% 50%; } |
| } |
| .animate-gradient { |
| background-size: 200% 200%; |
| animation: gradient 15s ease infinite; |
| } |
| .transition-all { transition: all 0.3s ease-in-out; } |
| .animate-scale { transition: transform 0.2s ease-in-out; } |
| .animate-scale:hover { transform: scale(1.02); } |
| .pulse-animation { animation: pulse 2s infinite; } |
| @keyframes pulse { |
| 0% { box-shadow: 0 0 0 0 rgba(156,39,176,0.4); } |
| 70% { box-shadow: 0 0 0 10px rgba(156,39,176,0); } |
| 100% { box-shadow: 0 0 0 0 rgba(156,39,176,0); } |
| } |
| @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; |
| } |
| |
| |
| |
| |
| .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; |
| 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; |
| } |
| |
| |
| .preview-table { |
| width: 100%; |
| border-collapse: collapse; |
| margin-top: 15px; |
| border-radius: 8px; |
| overflow: hidden; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.05); |
| } |
| |
| .preview-table th, .preview-table td { |
| padding: 12px 15px; |
| text-align: right; |
| border: 1px solid #e5e7eb; |
| } |
| |
| .preview-table th { |
| background-color: #f3f4f6; |
| font-weight: 600; |
| } |
| |
| .preview-table tr:nth-child(even) { |
| background-color: #f9fafb; |
| } |
| |
| .preview-table tr:hover { |
| background-color: #f0f9ff; |
| } |
| |
| |
| .perfect-match { |
| background-color: #d1fae5; |
| padding: 3px 8px; |
| border-radius: 4px; |
| font-weight: 500; |
| color: #065f46; |
| display: inline-block; |
| } |
| |
| |
| .text-selection-area { |
| position: relative; |
| margin-top: 10px; |
| } |
| |
| .text-selection-tools { |
| position: absolute; |
| top: -40px; |
| right: 0; |
| background-color: #374151; |
| border-radius: 8px; |
| box-shadow: 0 2px 8px rgba(0,0,0,0.2); |
| display: none; |
| z-index: 10; |
| animation: fadeInDown 0.3s; |
| } |
| |
| @keyframes fadeInDown { |
| from { opacity: 0; transform: translateY(-10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .text-selection-tools.visible { |
| display: flex; |
| } |
| |
| .selection-tool { |
| padding: 8px 12px; |
| color: white; |
| font-size: 14px; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| border-right: 1px solid #4b5563; |
| } |
| |
| .selection-tool:last-child { |
| border-right: none; |
| } |
| |
| .selection-tool:hover { |
| background-color: #4b5563; |
| } |
| |
| .selection-tool i { |
| margin-left: 6px; |
| } |
| |
| |
| .analysis-table { |
| width: 100%; |
| border-collapse: collapse; |
| margin-top: 20px; |
| border-radius: 8px; |
| overflow: hidden; |
| box-shadow: 0 2px 5px rgba(0,0,0,0.08); |
| } |
| |
| .analysis-table th, .analysis-table td { |
| padding: 12px 15px; |
| text-align: right; |
| } |
| |
| .analysis-table th { |
| background: linear-gradient(135deg, #3b82f6, #2563eb); |
| color: white; |
| font-weight: 600; |
| } |
| |
| .analysis-table tr { |
| border-bottom: 1px solid #e5e7eb; |
| } |
| |
| .analysis-table tr:last-child { |
| border-bottom: none; |
| } |
| |
| .analysis-table tbody tr:nth-child(even) { |
| background-color: #f8fafc; |
| } |
| |
| .analysis-table tbody tr:hover { |
| background-color: #f0f9ff; |
| } |
| |
| |
| @media print { |
| body { |
| padding: 0; |
| margin: 0; |
| } |
| |
| header, .logo-container, button, .view-selector, .error-filters, |
| .explanation-panel, #errorsList, #programExplanation, .view-btn { |
| display: none !important; |
| } |
| |
| .result-section { |
| break-inside: avoid; |
| } |
| |
| .segment-comparison { |
| break-inside: avoid; |
| page-break-inside: avoid; |
| margin: 15px 0; |
| border: 1px solid #ddd; |
| box-shadow: none; |
| } |
| |
| .text-comparison { |
| break-inside: avoid; |
| page-break-inside: avoid; |
| margin: 10px 0; |
| box-shadow: none; |
| border: 1px solid #ddd; |
| } |
| |
| .highlight-number, .highlight-missing, .highlight-meaning { |
| -webkit-print-color-adjust: exact; |
| print-color-adjust: exact; |
| } |
| } |
| |
| |
| .excel-preview { |
| background-color: #fff; |
| border: 1px solid #e5e7eb; |
| border-radius: 8px; |
| overflow: auto; |
| max-height: 500px; |
| margin: 15px 0; |
| } |
| |
| .excel-controls { |
| display: flex; |
| gap: 10px; |
| margin-bottom: 15px; |
| flex-wrap: wrap; |
| } |
| |
| .sheet-selector { |
| padding: 5px 10px; |
| background-color: #f3f4f6; |
| border: 1px solid #d1d5db; |
| border-radius: 5px; |
| cursor: pointer; |
| transition: all 0.2s; |
| } |
| |
| .sheet-selector:hover { |
| background-color: #e5e7eb; |
| } |
| |
| .sheet-selector.active { |
| background-color: #3b82f6; |
| color: white; |
| border-color: #2563eb; |
| } |
| |
| |
| |
| |
| .example-container { |
| border: 1px solid #e2e8f0; |
| border-radius: 10px; |
| overflow: hidden; |
| margin-bottom: 1.5rem; |
| box-shadow: 0 2px 8px rgba(0,0,0,0.05); |
| } |
| |
| .example-header { |
| background-color: #f8fafc; |
| padding: 12px 16px; |
| border-bottom: 1px solid #e2e8f0; |
| font-weight: 600; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| .example-header .badge { |
| font-size: 0.75rem; |
| font-weight: 500; |
| padding: 4px 8px; |
| border-radius: 9999px; |
| } |
| |
| .example-content { |
| padding: 16px; |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 12px; |
| } |
| |
| .example-source, .example-target { |
| padding: 12px; |
| border-radius: 6px; |
| line-height: 1.8; |
| } |
| |
| .example-source { |
| background-color: #f0f9ff; |
| border: 1px solid #bfdbfe; |
| } |
| |
| .example-target { |
| background-color: #fdf2f8; |
| border: 1px solid #fbcfe8; |
| } |
| |
| .example-action { |
| padding: 12px 16px; |
| background-color: #f8fafc; |
| border-top: 1px solid #e2e8f0; |
| display: flex; |
| justify-content: flex-end; |
| } |
| |
| .example-btn { |
| padding: 6px 12px; |
| background-color: #3b82f6; |
| color: white; |
| font-weight: 500; |
| border-radius: 4px; |
| font-size: 0.875rem; |
| cursor: pointer; |
| transition: all 0.2s; |
| } |
| |
| .example-btn:hover { |
| background-color: #2563eb; |
| } |
| </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> |
| </div> |
| </header> |
|
|
| |
| <section id="programExplanation" class="max-w-6xl mx-auto px-4 mb-8"> |
| <div class="bg-white rounded-2xl shadow-lg p-6 border border-gray-100"> |
| <h3 class="text-xl font-bold mb-3 text-blue-700 flex items-center"> |
| <i class="fas fa-info-circle ml-2"></i> نبذة مختصرة |
| </h3> |
| |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> |
| <div class="bg-yellow-50 p-4 rounded-lg border border-yellow-200 shadow-sm animate-scale hover:shadow-md transition-all"> |
| <div class="font-bold text-yellow-800 mb-2 flex items-center"> |
| <i class="fas fa-hashtag ml-2 text-yellow-600"></i> تحديد الأرقام |
| </div> |
| <p class="text-gray-700">اكتشاف الأرقام المختلفة بين النص المصدر والهدف بجميع أشكالها (عربية، هندية، إنجليزية).</p> |
| </div> |
| |
| <div class="bg-blue-50 p-4 rounded-lg border border-blue-200 shadow-sm animate-scale hover:shadow-md transition-all"> |
| <div class="font-bold text-blue-800 mb-2 flex items-center"> |
| <i class="fas fa-minus-circle ml-2 text-blue-600"></i> النصوص المفقودة |
| </div> |
| <p class="text-gray-700">تحديد النصوص الموجودة في المصدر والمفقودة في الترجمة، سواء كانت مفقودة كلياً أو جزئياً.</p> |
| </div> |
| |
| <div class="bg-red-50 p-4 rounded-lg border border-red-200 shadow-sm animate-scale hover:shadow-md transition-all"> |
| <div class="font-bold text-red-800 mb-2 flex items-center"> |
| <i class="fas fa-exclamation-circle ml-2 text-red-600"></i> اختلافات المعنى |
| </div> |
| <p class="text-gray-700">اكتشاف التغييرات في المعنى بين النص الأصلي والترجمة التي تؤثر على المضمون.</p> |
| </div> |
| </div> |
|
|
| <div class="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-100"> |
| <h4 class="font-bold text-blue-700 mb-2">طرق العرض المتعددة:</h4> |
| <div class="grid grid-cols-1 sm:grid-cols-3 gap-3"> |
| <div class="flex items-center"> |
| <i class="fas fa-columns text-blue-500 ml-2"></i> |
| <span>العرض الكلاسيكي</span> |
| </div> |
| <div class="flex items-center"> |
| <i class="fas fa-th-large text-blue-500 ml-2"></i> |
| <span>العرض المقسم</span> |
| </div> |
| <div class="flex items-center"> |
| <i class="fas fa-exchange-alt text-blue-500 ml-2"></i> |
| <span>العرض التفاعلي</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| </section> |
|
|
| |
| <section class="max-w-6xl mx-auto px-4 mb-8"> |
| <div class="bg-white rounded-2xl shadow-lg p-6 border border-gray-100"> |
| <h3 class="text-xl font-bold mb-5 text-blue-700 flex items-center"> |
| <i class="fas fa-lightbulb ml-2"></i> أمثلة توضيحية |
| </h3> |
|
|
| |
| <div class="example-container"> |
| <div class="example-header"> |
| <div class="flex items-center"> |
| <i class="fas fa-hashtag text-yellow-500 ml-2"></i> |
| <span>مثال على اختلافات الأرقام</span> |
| </div> |
| <span class="badge bg-yellow-100 text-yellow-800">أرقام</span> |
| </div> |
| <div class="example-content"> |
| <div class="example-source"> |
| في عام <span class="highlight-number">٢٠٢٣</span>، تم تنفيذ المشروع بتكلفة <span class="highlight-number">٥٠٠٠٠٠</span> ريال، وشارك فيه <span class="highlight-number">٢٥</span> موظفًا على مدار <span class="highlight-number">١٢</span> شهرًا. وتم توفير <span class="highlight-number">٣٠٪</span> من الميزانية المخصصة للمشروع. |
| </div> |
| <div class="example-target"> |
| In <span class="highlight-number">2022</span>, the project was implemented at a cost of <span class="highlight-number">450,000</span> riyals, with <span class="highlight-number">23</span> employees participating over <span class="highlight-number">10</span> months. <span class="highlight-number">35%</span> of the project's allocated budget was saved. |
| </div> |
| </div> |
| <div class="example-action"> |
| <button class="example-btn" onclick="useExample(1)">استخدام هذا المثال</button> |
| </div> |
| </div> |
|
|
| |
| <div class="example-container"> |
| <div class="example-header"> |
| <div class="flex items-center"> |
| <i class="fas fa-minus-circle text-blue-500 ml-2"></i> |
| <span>مثال على النص المفقود</span> |
| </div> |
| <span class="badge bg-blue-100 text-blue-800">نص مفقود</span> |
| </div> |
| <div class="example-content"> |
| <div class="example-source"> |
| يعتبر التعليم المستمر من أهم أدوات النجاح في عصرنا الحالي. ويشمل ذلك التعلم الذاتي والدورات التدريبية <span class="highlight-missing">والقراءة المستمرة</span>. كما أن <span class="highlight-missing">التطبيق العملي للمعرفة المكتسبة يساعد على ترسيخها وتطويرها</span>. |
| </div> |
| <div class="example-target"> |
| Continuous education is considered one of the most important tools for success in our current era. This includes self-learning and training courses. Practical application of acquired knowledge helps to consolidate and develop it. |
| </div> |
| </div> |
| <div class="example-action"> |
| <button class="example-btn" onclick="useExample(2)">استخدام هذا المثال</button> |
| </div> |
| </div> |
|
|
| |
| <div class="example-container"> |
| <div class="example-header"> |
| <div class="flex items-center"> |
| <i class="fas fa-exclamation-circle text-red-500 ml-2"></i> |
| <span>مثال على اختلاف المعنى</span> |
| </div> |
| <span class="badge bg-red-100 text-red-800">اختلاف معنى</span> |
| </div> |
| <div class="example-content"> |
| <div class="example-source"> |
| تتميز هذه السيارة بأنظمة <span class="highlight-meaning">أمان</span> متطورة تشمل نظام منع انغلاق المكابح ونظام التحكم الإلكتروني بالثبات. وتعمل بمحرك <span class="highlight-meaning">اقتصادي يوفر في استهلاك الوقود</span> بنسبة عالية. |
| </div> |
| <div class="example-target"> |
| This car features advanced <span class="highlight-meaning">entertainment</span> systems including an anti-lock braking system and electronic stability control. It runs on a <span class="highlight-meaning">high-performance engine that provides exceptional speed and acceleration</span>. |
| </div> |
| </div> |
| <div class="example-action"> |
| <button class="example-btn" onclick="useExample(3)">استخدام هذا المثال</button> |
| </div> |
| </div> |
| </div> |
| </section> |
|
|
| |
| <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="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 class="mt-2"> |
| <button id="processSourceBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> |
| معالجة OCR للسورس |
| </button> |
| </div> |
| </div> |
| |
| |
| <div class="text-center"> |
| <div class="group border-2 border-dashed border-blue-300 rounded-xl p-8 text-center hover:border-blue-500 transition-colors duration-300 bg-blue-50 hover:bg-blue-100"> |
| <label class="cursor-pointer block"> |
| <input type="file" id="targetFile" accept=".docx,.pdf,.jpg,.jpeg,.png,.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 class="mt-2"> |
| <button id="processTargetBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> |
| معالجة OCR للتارجت |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="processingStatus" class="hidden mt-4"> |
| <div class="alert alert-info bg-blue-50 border border-blue-200 rounded-xl p-4 my-4"> |
| <div class="flex items-center"> |
| <div class="animate-spin h-6 w-6 border-4 border-blue-600 rounded-full border-t-transparent ml-3"></div> |
| <span id="statusText">جاري معالجة الملف...</span> |
| </div> |
| </div> |
| <div class="progress mt-2"> |
| <div id="progressBar" class="progress-bar" style="width: 0%"></div> |
| </div> |
| </div> |
| |
| |
| <div id="pdfPagesCard" class="hidden mt-6"> |
| <h3 class="text-xl font-bold mb-4 text-gray-800">صفحات الملف</h3> |
| <p>اختر الصفحات التي تريد معالجتها (انقر للتحديد)</p> |
| |
| <div class="mb-3"> |
| <button id="selectAllBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-1 px-3 rounded text-sm ml-2">تحديد الكل</button> |
| <button id="deselectAllBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-1 px-3 rounded text-sm">إلغاء تحديد الكل</button> |
| </div> |
| |
| <div id="pdfPagesContainer" class="pdf-grid"></div> |
| |
| <div class="mt-4 flex items-center space-x-2"> |
| <div class="edit-toolbar flex flex-wrap gap-2"> |
| <button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="rotateLeft"> |
| <i class="fas fa-undo ml-1"></i> تدوير لليسار |
| </button> |
| <button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="rotateRight"> |
| <i class="fas fa-redo ml-1"></i> تدوير لليمين |
| </button> |
| <button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="flipHorizontal"> |
| <i class="fas fa-arrows-alt-h ml-1"></i> قلب أفقي |
| </button> |
| <button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="flipVertical"> |
| <i class="fas fa-arrows-alt-v ml-1"></i> قلب عمودي |
| </button> |
| <button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="cropImage"> |
| <i class="fas fa-crop ml-1"></i> قص الصورة |
| </button> |
| <button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="resetImage"> |
| <i class="fas fa-sync-alt ml-1"></i> إعادة ضبط |
| </button> |
| <button class="edit-tool px-3 py-1 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 transition-all" id="improveContrast"> |
| <i class="fas fa-adjust ml-1"></i> تحسين التباين |
| </button> |
| </div> |
| </div> |
| |
| <div class="mt-3"> |
| <button id="extractTextBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> |
| <i class="fas fa-magic ml-2"></i> استخراج النص |
| </button> |
| </div> |
| |
| |
| <div id="imageEditor" class="hidden mt-4"> |
| <div class="border border-gray-300 rounded-lg bg-gray-100 p-2"> |
| <canvas id="imageCanvas" class="max-w-full"></canvas> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="resultsCard" class="hidden mt-6"> |
| <h3 class="text-xl font-bold mb-4 text-gray-800">النص المستخرج</h3> |
| <div id="resultPreview"></div> |
| <div id="resultTextContainer" class="mt-4"> |
| <div class="flex justify-between items-center mb-2"> |
| <h4 class="font-bold text-gray-800">النص:</h4> |
| <button id="copyTextBtn" class="bg-blue-100 hover:bg-blue-200 text-blue-800 px-3 py-1 rounded-lg text-sm"> |
| <i class="far fa-copy ml-1"></i> نسخ النص |
| </button> |
| </div> |
| <div id="resultText" class="result-text"> |
| لم يتم استخراج نص بعد. |
| </div> |
| </div> |
| <div class="mt-3 flex space-x-2"> |
| <button id="useAsSourceBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> |
| استخدام النص كنص مصدر |
| </button> |
| <button id="useAsTargetBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition-all"> |
| استخدام النص كنص هدف |
| </button> |
| <button id="downloadTextBtn" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded transition-all"> |
| تنزيل النص |
| </button> |
| </div> |
| </div> |
| |
| |
| <div 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"> |
| |
| </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> النص المصدر |
| <span class="mr-2 text-sm font-normal text-gray-500">(اكتب أو استخدم أحد الأمثلة أعلاه)</span> |
| </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> النص الهدف |
| <span class="mr-2 text-sm font-normal text-gray-500">(اكتب أو استخدم أحد الأمثلة أعلاه)</span> |
| </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="classicView"> |
| <i class="fas fa-columns"></i> العرض الكلاسيكي |
| </div> |
| <div class="view-btn" 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> |
| |
| |
| <div id="classicView" class="view-container active classic-view-improved"> |
| <div class="result-section split-view"> |
| |
| <div> |
| <h4 class="text-lg font-bold text-gray-700 mb-3 flex items-center source-title"> |
| النص المصدر (مع التعليم) |
| </h4> |
| <div id="sourceTextReview" class="min-h-[200px] text-comparison"></div> |
| </div> |
| |
| <div> |
| <h4 class="text-lg font-bold text-gray-700 mb-3 flex items-center target-title"> |
| النص الهدف (مع التعليم) |
| </h4> |
| <div id="targetTextReview" class="min-h-[200px] text-comparison"></div> |
| </div> |
| </div> |
| |
| |
| <div class="explanation-panel mt-4"> |
| <h5>شرح الأخطاء</h5> |
| <div id="classicViewExplanation"></div> |
| </div> |
| </div> |
| |
| |
| <div id="segmentView" class="view-container"> |
| <div id="segmentedComparisonContainer" class="space-y-4"> |
| |
| </div> |
| |
| |
| <div class="explanation-panel mt-4"> |
| <h5>شرح الأخطاء في المقاطع</h5> |
| <div id="segmentViewExplanation"></div> |
| </div> |
| </div> |
| |
| |
| <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"> |
| |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="explanation-panel mt-4"> |
| <h5>التفاصيل والتوضيحات</h5> |
| <div id="interactiveViewExplanation"></div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="errorPopup" class="error-popup"> |
| <div class="error-popup-header"> |
| <span id="errorPopupTitle">تفاصيل الخطأ</span> |
| <button class="error-popup-close">×</button> |
| </div> |
| <div id="errorPopupContent" class="error-popup-body"> |
| |
| </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"> |
| |
| </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> |
| </main> |
| </div> |
|
|
| |
| |
| |
| <script> |
| |
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js'; |
| |
| |
| const DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions'; |
| const DEEPSEEK_API_KEY = 'sk-15606736ed9e4aea8b7cc11a195d2b01'; |
| |
| |
| const CHATGPT_API_URL = 'https://api.openai.com/v1/chat/completions'; |
| const CHATGPT_API_KEY = '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'; |
| |
| |
| const RAPIDAPI_KEY = '32769fb369mshfdf6f5e28e26674p1f3764jsn2a31085a1fc7'; |
| const OCR_API_URL = 'https://ocr43.p.rapidapi.com/v1/results'; |
| |
| |
| let fullAnalysisText = ''; |
| let analysisSegments = []; |
| let allDifferences = []; |
| let currentDiffIndex = -1; |
| |
| |
| let ocrPagesCount = 0; |
| let documentPages = []; |
| let selectedPages = []; |
| let extractedTexts = []; |
| let extractedPageNumbers = []; |
| let currentProcessingMode = ''; |
| |
| |
| let canvas = null; |
| let fabricCanvas = null; |
| let originalImageData = null; |
| let isInCropMode = false; |
| let cropRect = null; |
| let currentPageIndex = 0; |
| |
| |
| let sentenceErrors = []; |
| let errorExplanations = {}; |
| |
| |
| let currentErrorFilter = 'all'; |
| |
| |
| let excelWorkbook = null; |
| let currentSheetName = ''; |
| let excelData = null; |
| |
| |
| |
| |
| const examples = [ |
| |
| { |
| source: "في عام ٢٠٢٣، تم تنفيذ المشروع بتكلفة ٥٠٠٠٠٠ ريال، وشارك فيه ٢٥ موظفًا على مدار ١٢ شهرًا. وتم توفير ٣٠٪ من الميزانية المخصصة للمشروع.", |
| target: "In 2022, the project was implemented at a cost of 450,000 riyals, with 23 employees participating over 10 months. 35% of the project's allocated budget was saved." |
| }, |
| |
| { |
| source: "يعتبر التعليم المستمر من أهم أدوات النجاح في عصرنا الحالي. ويشمل ذلك التعلم الذاتي والدورات التدريبية والقراءة المستمرة. كما أن التطبيق العملي للمعرفة المكتسبة يساعد على ترسيخها وتطويرها.", |
| target: "Continuous education is considered one of the most important tools for success in our current era. This includes self-learning and training courses. Practical application of acquired knowledge helps to consolidate and develop it." |
| }, |
| |
| { |
| source: "تتميز هذه السيارة بأنظمة أمان متطورة تشمل نظام منع انغلاق المكابح ونظام التحكم الإلكتروني بالثبات. وتعمل بمحرك اقتصادي يوفر في استهلاك الوقود بنسبة عالية.", |
| target: "This car features advanced entertainment systems including an anti-lock braking system and electronic stability control. It runs on a high-performance engine that provides exceptional speed and acceleration." |
| } |
| ]; |
| |
| |
| function useExample(exampleIndex) { |
| if (exampleIndex >= 1 && exampleIndex <= examples.length) { |
| const example = examples[exampleIndex - 1]; |
| document.getElementById('sourceText').value = example.source; |
| document.getElementById('targetText').value = example.target; |
| |
| |
| setTimeout(() => { |
| document.getElementById('submitBtn').click(); |
| }, 500); |
| } |
| } |
| |
| |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| const savedCount = localStorage.getItem('ocrPagesCount'); |
| const lastDate = localStorage.getItem('lastOcrDate'); |
| |
| if (savedCount) { |
| ocrPagesCount = parseInt(savedCount); |
| document.getElementById('ocrCounter').textContent = ocrPagesCount; |
| } |
| |
| if (lastDate) { |
| document.getElementById('lastOcrDate').textContent = lastDate; |
| } |
| |
| |
| initErrorPopup(); |
| |
| |
| document.getElementById('processSourceBtn').addEventListener('click', function() { |
| processFileForOCR('source'); |
| }); |
| |
| document.getElementById('processTargetBtn').addEventListener('click', function() { |
| processFileForOCR('target'); |
| }); |
| |
| |
| document.getElementById('selectAllBtn')?.addEventListener('click', selectAllPages); |
| document.getElementById('deselectAllBtn')?.addEventListener('click', deselectAllPages); |
| document.getElementById('extractTextBtn')?.addEventListener('click', extractText); |
| |
| |
| document.getElementById('copyTextBtn')?.addEventListener('click', copyText); |
| document.getElementById('downloadTextBtn')?.addEventListener('click', downloadText); |
| document.getElementById('useAsSourceBtn')?.addEventListener('click', function() { |
| useOcrText('source'); |
| }); |
| document.getElementById('useAsTargetBtn')?.addEventListener('click', function() { |
| useOcrText('target'); |
| }); |
| |
| |
| document.getElementById('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('sourceFile').addEventListener('change', function(event) { |
| const file = event.target.files[0]; |
| if (file && (file.name.endsWith('.xlsx') || file.name.endsWith('.xls'))) { |
| processExcelFile(file, 'source'); |
| } |
| }); |
| |
| document.getElementById('targetFile').addEventListener('change', function(event) { |
| const file = event.target.files[0]; |
| if (file && (file.name.endsWith('.xlsx') || file.name.endsWith('.xls'))) { |
| processExcelFile(file, 'target'); |
| } |
| }); |
| |
| document.getElementById('sourceExtraFile').addEventListener('change', function(event) { |
| const file = event.target.files[0]; |
| if (file && (file.name.endsWith('.xlsx') || file.name.endsWith('.xls'))) { |
| processExcelFile(file, 'extra'); |
| } |
| }); |
| |
| |
| document.querySelectorAll('.view-btn').forEach(btn => { |
| btn.addEventListener('click', function() { |
| |
| 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() { |
| |
| document.querySelectorAll('.error-filter').forEach(f => f.classList.remove('active')); |
| |
| |
| this.classList.add('active'); |
| |
| |
| currentErrorFilter = this.getAttribute('data-filter'); |
| |
| |
| applyErrorFilter(currentErrorFilter); |
| }); |
| }); |
| |
| |
| document.querySelectorAll('.example-btn').forEach(btn => { |
| btn.addEventListener('click', function() { |
| const exampleId = parseInt(this.getAttribute('data-example')); |
| useExample(exampleId); |
| }); |
| }); |
| }); |
| |
| |
| |
| |
| function processExcelFile(file, targetType) { |
| document.getElementById('processingStatus').classList.remove('hidden'); |
| document.getElementById('statusText').textContent = 'جاري معالجة ملف Excel...'; |
| document.getElementById('progressBar').style.width = '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%'; |
| |
| if (excelWorkbook.SheetNames.length > 0) { |
| |
| renderSheetSelector(excelWorkbook.SheetNames, targetType); |
| |
| |
| selectExcelSheet(excelWorkbook.SheetNames[0], targetType); |
| } else { |
| alert('لم يتم العثور على أوراق في ملف Excel'); |
| } |
| |
| document.getElementById('progressBar').style.width = '100%'; |
| setTimeout(() => { |
| document.getElementById('processingStatus').classList.add('hidden'); |
| }, 500); |
| } catch (error) { |
| console.error('خطأ في معالجة ملف Excel:', error); |
| alert('حدث خطأ أثناء معالجة ملف Excel'); |
| document.getElementById('processingStatus').classList.add('hidden'); |
| } |
| }; |
| |
| reader.onerror = function() { |
| alert('حدث خطأ أثناء قراءة الملف'); |
| document.getElementById('processingStatus').classList.add('hidden'); |
| }; |
| |
| reader.readAsArrayBuffer(file); |
| } |
| |
| |
| |
| |
| 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'); |
| } |
| |
| |
| document.getElementById('excelPreviewCard').classList.remove('hidden'); |
| } |
| |
| |
| |
| |
| 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); |
| } |
| |
| |
| |
| |
| 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; |
| } |
| |
| |
| |
| |
| function useExcelContent(target) { |
| if (!excelData || !excelData.data) { |
| alert('لم يتم تحميل بيانات Excel'); |
| return; |
| } |
| |
| |
| let textContent = ''; |
| |
| |
| 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 initErrorPopup() { |
| |
| document.querySelector('.error-popup-close').addEventListener('click', function() { |
| const popup = document.getElementById('errorPopup'); |
| popup.classList.remove('show'); |
| setTimeout(() => { |
| popup.style.display = 'none'; |
| }, 300); |
| }); |
| |
| |
| window.addEventListener('click', function(event) { |
| const popup = document.getElementById('errorPopup'); |
| if (event.target === popup) { |
| popup.classList.remove('show'); |
| setTimeout(() => { |
| popup.style.display = 'none'; |
| }, 300); |
| } |
| }); |
| } |
| |
| |
| |
| |
| function showErrorExplanation(errorType, errorText, explanation) { |
| const popup = document.getElementById('errorPopup'); |
| const title = document.getElementById('errorPopupTitle'); |
| const content = document.getElementById('errorPopupContent'); |
| |
| |
| let typeIcon = ''; |
| let typeClass = ''; |
| |
| if (errorType === 'number') { |
| title.textContent = 'خطأ في الأرقام'; |
| typeIcon = '<i class="fas fa-hashtag text-yellow-500 ml-2"></i>'; |
| typeClass = 'bg-yellow-50 border-yellow-400'; |
| } else if (errorType === 'missing') { |
| title.textContent = 'نص مفقود'; |
| typeIcon = '<i class="fas fa-minus-circle text-blue-500 ml-2"></i>'; |
| typeClass = 'bg-blue-50 border-blue-400'; |
| } else if (errorType === 'meaning') { |
| title.textContent = 'اختلاف في المعنى'; |
| typeIcon = '<i class="fas fa-exclamation-circle text-red-500 ml-2"></i>'; |
| typeClass = 'bg-red-50 border-red-400'; |
| } else { |
| title.textContent = 'تفاصيل الخطأ'; |
| typeIcon = '<i class="fas fa-info-circle text-blue-500 ml-2"></i>'; |
| typeClass = 'bg-blue-50 border-blue-400'; |
| } |
| |
| |
| content.innerHTML = ` |
| <div class="mb-4"> |
| <strong class="block mb-2 flex items-center text-gray-700"> |
| ${typeIcon} النص المحدد: |
| </strong> |
| <div class="p-3 rounded-lg border-r-4 ${typeClass}">${errorText}</div> |
| </div> |
| <div> |
| <strong class="block mb-2 flex items-center text-gray-700"> |
| <i class="fas fa-lightbulb text-yellow-500 ml-2"></i> الشرح: |
| </strong> |
| <div class="bg-blue-50 p-3 rounded-lg border border-blue-200">${explanation}</div> |
| </div> |
| <div class="mt-4 p-3 bg-gray-50 rounded-lg flex items-center text-sm text-gray-500"> |
| <i class="fas fa-info-circle ml-2"></i> |
| انقر خارج النافذة أو اضغط على زر الإغلاق للرجوع |
| </div> |
| `; |
| |
| |
| popup.style.display = 'block'; |
| setTimeout(() => { |
| popup.classList.add('show'); |
| }, 10); |
| } |
| |
| |
| |
| |
| 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()); |
| } |
| |
| |
| |
| |
| async function processFile(file) { |
| let text = ""; |
| if (file.type === 'application/pdf') { |
| try { |
| |
| const arrayBuffer = await file.arrayBuffer(); |
| const loadingTask = pdfjsLib.getDocument(arrayBuffer); |
| const pdf = await loadingTask.promise; |
| |
| const numPages = pdf.numPages; |
| let fullText = ""; |
| |
| for (let i = 1; i <= numPages; i++) { |
| const page = await pdf.getPage(i); |
| const content = await page.getTextContent(); |
| const pageText = content.items.map(item => item.str).join(' '); |
| fullText += pageText + "\n\n"; |
| } |
| |
| text = fullText; |
| } catch (error) { |
| console.error('خطأ في استخراج النص من PDF:', error); |
| throw new Error('فشل استخراج النص من ملف PDF'); |
| } |
| } else if (file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') { |
| try { |
| const arrayBuffer = await file.arrayBuffer(); |
| const result = await mammoth.extractRawText({ arrayBuffer }); |
| text = result.value; |
| } catch (error) { |
| console.error('خطأ في استخراج النص من DOCX:', error); |
| throw new Error('فشل استخراج النص من ملف Word'); |
| } |
| } else if (file.type === 'application/vnd.ms-excel' || |
| file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') { |
| try { |
| const arrayBuffer = await file.arrayBuffer(); |
| const workbook = XLSX.read(arrayBuffer, {type: 'array'}); |
| |
| |
| const firstSheet = workbook.SheetNames[0]; |
| const worksheet = workbook.Sheets[firstSheet]; |
| const data = XLSX.utils.sheet_to_json(worksheet, {header: 1}); |
| |
| |
| text = data.map(row => row.join('\t')).join('\n'); |
| } catch (error) { |
| console.error('خطأ في استخراج النص من Excel:', error); |
| throw new Error('فشل استخراج النص من ملف Excel'); |
| } |
| } else { |
| throw new Error('نوع الملف غير مدعوم'); |
| } |
| return text; |
| } |
| |
| |
| |
| |
| |
| document.getElementById('sourceFile')?.addEventListener('change', async (event) => { |
| const file = event.target.files[0]; |
| if (!file) return; |
| |
| |
| if (file.type === 'application/pdf' || file.type.startsWith('image/') || |
| file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { |
| return; |
| } |
| |
| document.getElementById('processingStatus').classList.remove('hidden'); |
| document.getElementById('statusText').textContent = 'جاري معالجة ملف السورس...'; |
| document.getElementById('progressBar').style.width = '30%'; |
| |
| try { |
| const text = await processFile(file); |
| document.getElementById('progressBar').style.width = '90%'; |
| document.getElementById('sourceText').value = text; |
| document.getElementById('statusText').textContent = 'تم استخراج النص بنجاح!'; |
| document.getElementById('progressBar').style.width = '100%'; |
| |
| |
| setTimeout(() => { |
| document.getElementById('processingStatus').classList.add('hidden'); |
| }, 800); |
| } catch (error) { |
| console.error('Error processing source file:', error); |
| addError('خطأ في معالجة ملف السورس: ' + error.message); |
| document.getElementById('processingStatus').classList.add('hidden'); |
| } |
| }); |
| |
| |
| document.getElementById('targetFile')?.addEventListener('change', async (event) => { |
| const file = event.target.files[0]; |
| if (!file) return; |
| |
| |
| if (file.type === 'application/pdf' || file.type.startsWith('image/') || |
| file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { |
| return; |
| } |
| |
| document.getElementById('processingStatus').classList.remove('hidden'); |
| document.getElementById('statusText').textContent = 'جاري معالجة ملف التارجت...'; |
| document.getElementById('progressBar').style.width = '30%'; |
| |
| try { |
| const text = await processFile(file); |
| document.getElementById('progressBar').style.width = '90%'; |
| document.getElementById('targetText').value = text; |
| document.getElementById('statusText').textContent = 'تم استخراج النص بنجاح!'; |
| document.getElementById('progressBar').style.width = '100%'; |
| |
| |
| setTimeout(() => { |
| document.getElementById('processingStatus').classList.add('hidden'); |
| }, 800); |
| } catch (error) { |
| console.error('Error processing target file:', error); |
| addError('خطأ في معالجة ملف التارجت: ' + error.message); |
| document.getElementById('processingStatus').classList.add('hidden'); |
| } |
| }); |
| |
| |
| document.getElementById('sourceExtraFile')?.addEventListener('change', async (event) => { |
| const file = event.target.files[0]; |
| if (!file) return; |
| |
| |
| if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { |
| return; |
| } |
| |
| document.getElementById('processingStatus').classList.remove('hidden'); |
| document.getElementById('statusText').textContent = 'جاري معالجة ملف المصدر الإضافي...'; |
| document.getElementById('progressBar').style.width = '30%'; |
| |
| try { |
| const text = await processFile(file); |
| document.getElementById('progressBar').style.width = '90%'; |
| document.getElementById('sourceExtraText').value = text; |
| document.getElementById('statusText').textContent = 'تم استخراج النص بنجاح!'; |
| document.getElementById('progressBar').style.width = '100%'; |
| |
| |
| setTimeout(() => { |
| document.getElementById('processingStatus').classList.add('hidden'); |
| }, 800); |
| } catch (error) { |
| console.error('Error processing extra source file:', error); |
| addError('خطأ في معالجة ملف المصدر الإضافي: ' + error.message); |
| document.getElementById('processingStatus').classList.add('hidden'); |
| } |
| }); |
| |
| |
| |
| |
| 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 { |
| 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 highlightSentenceErrors(text, errors) { |
| if (!errors || errors.length === 0) return text; |
| |
| |
| let highlightedText = text; |
| |
| |
| const sortedErrors = [...errors].sort((a, b) => |
| b.errorText.length - a.errorText.length |
| ); |
| |
| |
| for (const error of sortedErrors) { |
| |
| let highlightClass = ''; |
| let icon = ''; |
| |
| if (error.type === 'number') { |
| highlightClass = 'highlight-number'; |
| icon = 'hashtag'; |
| } else if (error.type === 'missing') { |
| highlightClass = 'highlight-missing'; |
| icon = 'minus-circle'; |
| } else if (error.type === 'meaning') { |
| highlightClass = 'highlight-meaning'; |
| icon = 'exclamation-circle'; |
| } |
| |
| |
| const errorId = `error-${Math.random().toString(36).substring(2, 9)}`; |
| const explanation = encodeURIComponent(error.explanation || 'لا يوجد شرح متاح'); |
| const reference = encodeURIComponent(error.reference || 'لا يوجد مرجع متاح'); |
| |
| |
| const replacement = `<span class="${highlightClass}" data-error-id="${errorId}" data-error-type="${error.type}" data-explanation="${explanation}" data-reference="${reference}" title="انقر للتفاصيل">${error.errorText}</span>`; |
| |
| |
| const sentenceIndex = highlightedText.indexOf(error.sentence); |
| if (sentenceIndex !== -1) { |
| |
| const beforeSentence = highlightedText.substring(0, sentenceIndex); |
| const afterSentence = highlightedText.substring(sentenceIndex + error.sentence.length); |
| |
| |
| let updatedSentence = error.sentence; |
| const errorTextIndex = updatedSentence.indexOf(error.errorText); |
| if (errorTextIndex !== -1) { |
| updatedSentence = |
| updatedSentence.substring(0, errorTextIndex) + |
| replacement + |
| updatedSentence.substring(errorTextIndex + error.errorText.length); |
| } |
| |
| |
| highlightedText = |
| beforeSentence + |
| `<span class="sentence-with-error" data-error-id="${errorId}">${updatedSentence}</span>` + |
| afterSentence; |
| } else { |
| |
| highlightedText = highlightedText.replace( |
| new RegExp(escapeRegExp(error.errorText), 'g'), |
| replacement |
| ); |
| } |
| } |
| |
| 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++; |
| } |
| } |
| |
| |
| 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 countWords(text) { |
| return text.trim().split(/\s+/).filter(word => word !== "").length; |
| } |
| |
| |
| function escapeRegExp(string) { |
| return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| } |
| |
| |
| function splitIntoLines(text) { |
| return text.split('\n').map((line, i) => { |
| |
| return ` |
| <div class="line-item"> |
| <span class="line-number">${i+1}</span> |
| <span class="line-text">${line || ' '}</span> |
| </div>`; |
| }).join(''); |
| } |
| |
| |
| function getLineNumber(text, substring) { |
| const index = text.indexOf(substring); |
| if (index === -1) return "غير محدد"; |
| return text.substring(0, index).split("\n").length; |
| } |
| |
| |
| function truncateText(text, maxLength) { |
| if (text.length <= maxLength) return text; |
| return text.substring(0, maxLength) + '...'; |
| } |
| |
| |
| function formatAnalysisText(text) { |
| |
| text = text.replace(/الأرقام/g, '<span class="font-bold text-blue-600">الأرقام</span>'); |
| text = text.replace(/المفقودة/g, '<span class="font-bold text-blue-600">المفقودة</span>'); |
| text = text.replace(/المعنى/g, '<span class="font-bold text-blue-600">المعنى</span>'); |
| |
| |
| text = text.replace(/<([^<>]+)>/g, '<span class="highlight-number"><$1></span>'); |
| text = text.replace(/__(.*?)__/g, '<span class="highlight-missing">__$1__</span>'); |
| text = text.replace(/\[MEANING\](.*?)\[\/MEANING\]/g, '<span class="highlight-meaning">$1</span>'); |
| |
| return text; |
| } |
| |
| |
| function highlightSyncWord(text, syncWord) { |
| if (!syncWord || !text.includes(syncWord)) return text; |
| |
| |
| const regex = new RegExp(`(\\b${escapeRegExp(syncWord)}\\b)`, 'g'); |
| |
| const lastIndex = text.lastIndexOf(syncWord); |
| if (lastIndex !== -1) { |
| const beforeSync = text.substring(0, lastIndex); |
| const afterSync = text.substring(lastIndex + syncWord.length); |
| return beforeSync + '<span class="sync-word">' + syncWord + '</span>' + afterSync; |
| } |
| |
| return text; |
| } |
| |
| |
| |
| |
| |
| function applyHighlights(originalText, targetText, analysisOutput) { |
| |
| 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 sentences = splitIntoSentences(enhancedSourceText); |
| |
| |
| const highlightedSentences = sentences.map(sentence => { |
| let hasError = false; |
| let highlightedSentence = sentence; |
| |
| |
| numberMatches.forEach(phrase => { |
| if (sentence.includes(phrase)) { |
| |
| const normalizedPhrase = normalizeNumbers(phrase); |
| const normalizedSentence = normalizeNumbers(sentence); |
| |
| if (normalizedSentence.includes(normalizedPhrase)) { |
| |
| } else { |
| 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; |
| } |
| } |
| }); |
| |
| |
| missingMatches.forEach(phrase => { |
| 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; |
| } |
| }); |
| |
| |
| meaningMatches.forEach(phrase => { |
| 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; |
| } |
| }); |
| |
| |
| if (hasError) { |
| return `<span class="sentence-with-error">${highlightedSentence}</span>`; |
| } |
| |
| return highlightedSentence; |
| }); |
| |
| return highlightedSentences.join(' '); |
| } |
| |
| |
| |
| |
| function applyErrorFilter(filterType) { |
| |
| applyClassicViewFilter(filterType); |
| |
| |
| applySegmentedViewFilter(filterType); |
| |
| |
| applyInteractiveViewFilter(filterType); |
| } |
| |
| |
| function applyClassicViewFilter(filterType) { |
| |
| if (filterType === 'all') { |
| document.querySelectorAll('#sourceTextReview .highlight-number, #sourceTextReview .highlight-missing, #sourceTextReview .highlight-meaning, #targetTextReview .highlight-number, #targetTextReview .highlight-missing, #targetTextReview .highlight-meaning, #sourceTextReview .completely-missing, #sourceTextReview .partially-missing').forEach(el => { |
| el.style.display = ''; |
| }); |
| |
| document.querySelectorAll('.sentence-with-error').forEach(el => { |
| el.classList.remove('opacity-50'); |
| }); |
| return; |
| } |
| |
| |
| document.querySelectorAll('#sourceTextReview .highlight-number, #sourceTextReview .highlight-missing, #sourceTextReview .highlight-meaning, #targetTextReview .highlight-number, #targetTextReview .highlight-missing, #targetTextReview .highlight-meaning, #sourceTextReview .completely-missing, #sourceTextReview .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(`#sourceTextReview ${selector}, #targetTextReview ${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'); |
| }); |
| } |
| }); |
| } |
| |
| |
| function applySegmentedViewFilter(filterType) { |
| |
| const segments = document.querySelectorAll('.segment-comparison'); |
| |
| if (filterType === 'all') { |
| |
| segments.forEach(segment => { |
| segment.style.display = ''; |
| |
| segment.querySelectorAll('.highlight-number, .highlight-missing, .highlight-meaning, .completely-missing, .partially-missing').forEach(el => { |
| el.style.display = ''; |
| }); |
| |
| segment.querySelectorAll('.sentence-with-error').forEach(el => { |
| el.classList.remove('opacity-50'); |
| }); |
| }); |
| return; |
| } |
| |
| |
| segments.forEach(segment => { |
| segment.querySelectorAll('.highlight-number, .highlight-missing, .highlight-meaning, .completely-missing, .partially-missing').forEach(el => { |
| el.style.display = 'none'; |
| }); |
| |
| |
| segment.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'; |
| } |
| |
| |
| const highlightsToShow = segment.querySelectorAll(selector); |
| highlightsToShow.forEach(el => { |
| el.style.display = ''; |
| |
| const errorId = el.getAttribute('data-error-id'); |
| if (errorId) { |
| segment.querySelectorAll(`.sentence-with-error[data-error-id="${errorId}"]`).forEach(sentence => { |
| sentence.classList.remove('opacity-50'); |
| }); |
| } |
| }); |
| |
| |
| if (highlightsToShow.length === 0) { |
| segment.style.display = 'none'; |
| } else { |
| segment.style.display = ''; |
| } |
| }); |
| } |
| |
| |
| function applyInteractiveViewFilter(filterType) { |
| |
| if (filterType === 'all') { |
| |
| currentDiffIndex = 0; |
| displayCurrentDifference(); |
| return; |
| } |
| |
| |
| const filteredDiffs = allDifferences.filter(diff => diff.type === filterType); |
| |
| |
| if (filteredDiffs.length === 0) { |
| document.getElementById('currentDiffDisplay').innerHTML = ` |
| <div class="flex flex-col items-center justify-center py-8"> |
| <div class="text-5xl text-gray-300 mb-4"> |
| <i class="fas fa-${filterType === 'number' ? 'hashtag' : filterType === 'missing' ? 'minus-circle' : 'exclamation-circle'}"></i> |
| </div> |
| <p class="text-gray-500 text-center">لا توجد اختلافات من نوع ${filterType === 'number' ? 'الأرقام' : filterType === 'missing' ? 'النصوص المفقودة' : 'المعنى'}</p> |
| </div>`; |
| document.getElementById('diffCounter').textContent = "0/0"; |
| |
| |
| document.getElementById('diffDetailedView').classList.add('hidden'); |
| document.getElementById('diffReference').classList.add('hidden'); |
| |
| |
| document.getElementById('prevDiff').disabled = true; |
| document.getElementById('nextDiff').disabled = true; |
| } else { |
| |
| currentDiffIndex = allDifferences.findIndex(diff => diff.type === filterType); |
| |
| |
| displayCurrentDifference(); |
| |
| |
| document.getElementById('prevDiff').disabled = false; |
| document.getElementById('nextDiff').disabled = false; |
| } |
| } |
| |
| |
| |
| |
| async function alignTextsWithModel(sourceText, targetText) { |
| try { |
| |
| document.getElementById('processingStatus').classList.remove('hidden'); |
| document.getElementById('statusText').textContent = 'جاري تقسيم ومزامنة النصوص ...'; |
| document.getElementById('progressBar').style.width = '30%'; |
| |
| |
| const prompt = `أنت خبير في تقسيم النصوص ومزامنتها. مهمتك تجزئة النصين التاليين (المصدر والهدف) إلى فقرات متوازية |
| بحيث تتزامن كل فقرة في المصدر مع الفقرة المقابلة لها في الهدف من حيث المعنى والمحتوى. |
| |
| يجب أن تتبع القواعد التالية: |
| 1. حتى لو كان هناك نص مفقود في أحد النصين، ابحث عن أقرب مقابل له |
| 2. قسّم النصوص إلى أجزاء متزامنة لا تزيد عن 500 كلمة لكل جزء |
| 3. حاول مطابقة العناوين والأرقام والمصطلحات المشتركة بين النصين |
| |
| أرجع النتائج بصيغة JSON: |
| { |
| "segments": [ |
| { |
| "source": "نص المصدر للفقرة الأولى", |
| "target": "نص الهدف المقابل للفقرة الأولى" |
| }, |
| { |
| "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%'; |
| |
| const response = await fetch(DEEPSEEK_API_URL, { |
| method: 'POST', |
| headers: { |
| 'Authorization': 'Bearer ' + DEEPSEEK_API_KEY, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify(payload) |
| }); |
| |
| if (!response.ok) { |
| throw new Error('فشل استدعاء نموذج لتقسيم النصوص: ' + response.statusText); |
| } |
| |
| document.getElementById('progressBar').style.width = '80%'; |
| |
| const data = await response.json(); |
| const result = data.choices[0].message.content.trim(); |
| |
| |
| 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); |
| } |
| |
| document.getElementById('progressBar').style.width = '100%'; |
| document.getElementById('statusText').textContent = 'تم تقسيم النصوص بنجاح!'; |
| |
| |
| setTimeout(() => { |
| document.getElementById('processingStatus').classList.add('hidden'); |
| }, 1000); |
| |
| return segments; |
| } catch (error) { |
| console.error('خطأ في تقسيم النصوص:', error); |
| |
| document.getElementById('processingStatus').classList.add('hidden'); |
| addError('حدث خطأ أثناء تقسيم النصوص: ' + error.message); |
| |
| |
| return createFallbackSegments(sourceText, targetText); |
| } |
| } |
| |
| |
| 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()); |
| |
| const segments = []; |
| const maxLength = 500; |
| |
| |
| let sourceIndex = 0; |
| let targetIndex = 0; |
| |
| while (sourceIndex < sourceParagraphs.length && targetIndex < targetParagraphs.length) { |
| let currentSource = ''; |
| let currentTarget = ''; |
| let currentWordCount = 0; |
| |
| const initialSourceIndex = sourceIndex; |
| const initialTargetIndex = targetIndex; |
| |
| |
| while (sourceIndex < sourceParagraphs.length && |
| targetIndex < targetParagraphs.length && |
| currentWordCount < maxLength) { |
| |
| const sourcePara = sourceParagraphs[sourceIndex]; |
| const targetPara = targetParagraphs[targetIndex]; |
| |
| const sourceWords = sourcePara.split(/\s+/).length; |
| const targetWords = targetPara.split(/\s+/).length; |
| |
| |
| if (currentWordCount + Math.max(sourceWords, targetWords) > maxLength && currentWordCount > 0) { |
| break; |
| } |
| |
| |
| currentSource += (currentSource ? '\n\n' : '') + sourcePara; |
| currentTarget += (currentTarget ? '\n\n' : '') + targetPara; |
| currentWordCount += Math.max(sourceWords, targetWords); |
| |
| sourceIndex++; |
| targetIndex++; |
| } |
| |
| |
| if (sourceIndex === initialSourceIndex && targetIndex === initialTargetIndex) { |
| break; |
| } |
| |
| |
| if (currentSource || currentTarget) { |
| segments.push({ source: currentSource, target: currentTarget }); |
| } |
| } |
| |
| |
| if (sourceIndex < sourceParagraphs.length) { |
| const remainingSource = sourceParagraphs.slice(sourceIndex).join('\n\n'); |
| segments.push({ source: remainingSource, target: 'نص مفقود في الترجمة' }); |
| } |
| |
| |
| if (targetIndex < targetParagraphs.length) { |
| const remainingTarget = targetParagraphs.slice(targetIndex).join('\n\n'); |
| segments.push({ source: 'نص إضافي في الترجمة', target: remainingTarget }); |
| } |
| |
| return segments; |
| } |
| |
| |
| |
| |
| async function analyzeAlignedPair(sourceText, targetText, pairNumber) { |
| try { |
| |
| document.getElementById('statusText').textContent = `جاري تحليل القسم ${pairNumber}...`; |
| |
| |
| const prompt = `قارن النص المصدر والنص الهدف التاليين، وحدد بدقة: |
| 1. اختلافات الأرقام: ضع الرقم المختلف بين علامتي < > |
| 2. النصوص المفقودة: ضع النص المفقود في الترجمة بين علامتي __ __ |
| • تأكد من تحديد النص المفقود تماما بشكل منفصل |
| • إذا كان جزء من النص مفقود، حدده بدقة |
| 3. اختلافات المعنى: ضع النص الذي تغير معناه بين علامتي [MEANING] [/MEANING] |
| |
| اعتبر الأرقام بمختلف أنظمتها (العربية والهندية والإنجليزية) متطابقة إذا كانت تمثل نفس الرقم. |
| قم بتحليل كل جملة على حدة وتحديد الاختلافات بدقة. |
| |
| لكل اختلاف، قدم شرحاً موجزاً للخطأ والتصحيح المقترح. |
| انتبه بشكل خاص للنصوص المفقودة تماما في الترجمة وحددها. |
| |
| النص المصدر: |
| ${sourceText} |
| |
| النص الهدف: |
| ${targetText}`; |
| |
| const payload = { |
| model: "deepseek-reasoner", |
| messages: [ |
| { role: "system", content: "أنت خبير لغوي في تحليل ومقارنة النصوص المترجمة بدقة عالية." }, |
| { role: "user", content: prompt } |
| ], |
| temperature: 0.2, |
| max_tokens: 2048 |
| }; |
| |
| |
| const response = await fetch(DEEPSEEK_API_URL, { |
| method: 'POST', |
| headers: { |
| 'Authorization': 'Bearer ' + DEEPSEEK_API_KEY, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify(payload) |
| }); |
| |
| if (!response.ok) { |
| throw new Error('فشل استدعاء للتحليل: ' + response.statusText); |
| } |
| |
| const data = await response.json(); |
| const 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> |
| <i class="fas fa-chevron-down"></i> |
| `; |
| |
| |
| const contentDiv = document.createElement('div'); |
| contentDiv.className = 'section-content'; |
| |
| |
| contentDiv.innerHTML = ` |
| <div class="aligned-paragraphs mb-4"> |
| <div class="paragraph-ar">${segment.source}</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> |
| `; |
| } |
| } |
| |
| |
| |
| |
| document.getElementById('submitBtn').addEventListener('click', async () => { |
| const sourceText = document.getElementById('sourceText').value; |
| const targetText = document.getElementById('targetText').value; |
| |
| |
| document.getElementById('errorsList').innerHTML = ''; |
| document.getElementById('resultSection').classList.remove('hidden'); |
| |
| if (!sourceText || !targetText) { |
| addError('يرجى إدخال كلا النصين المصدر والهدف'); |
| return; |
| } |
| |
| |
| const sourceWordCount = countWords(sourceText); |
| const targetWordCount = countWords(targetText); |
| if (Math.abs(sourceWordCount - targetWordCount) > sourceWordCount * 0.2) { |
| addError(`عدد كلمات النص المصدر (${sourceWordCount}) يختلف بشكل كبير عن النص الهدف (${targetWordCount})`, 'warning'); |
| } |
| |
| try { |
| addError('جارٍ تقسيم النصوص باستخدام المقسم...', 'info'); |
| document.getElementById('processingStatus').classList.remove('hidden'); |
| |
| |
| 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'); |
| |
| let totalNumberErrors = 0; |
| let totalMissingErrors = 0; |
| let totalMeaningErrors = 0; |
| |
| |
| for (let i = 0; i < segments.length; i++) { |
| document.getElementById('progressBar').style.width = `${((i + 1) / segments.length) * 100}%`; |
| document.getElementById('statusText').textContent = `جاري تحليل القسم ${i+1} من ${segments.length}...`; |
| |
| |
| 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)); |
| } |
| |
| |
| 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'); |
| } |
| |
| |
| displayClassicView(sourceText, targetText); |
| |
| |
| displaySegmentedView(); |
| |
| |
| setupInteractiveView(); |
| |
| } catch (error) { |
| console.error('Error during analysis:', error); |
| addError('حدث خطأ أثناء التحليل: ' + error.message, 'error'); |
| } finally { |
| document.getElementById('processingStatus').classList.add('hidden'); |
| } |
| }); |
| |
| |
| |
| |
| function displayClassicView(sourceText, targetText) { |
| |
| let combinedAnalysis = ''; |
| analysisSegments.forEach((segment, index) => { |
| if (index > 0) combinedAnalysis += '\n\n---\n\n'; |
| combinedAnalysis += segment.analysis; |
| }); |
| |
| |
| document.getElementById('sourceTextReview').innerHTML = applyHighlights(sourceText, targetText, combinedAnalysis); |
| document.getElementById('targetTextReview').innerHTML = applyHighlights(targetText, sourceText, combinedAnalysis); |
| |
| |
| document.getElementById('classicViewExplanation').innerHTML = generateExplanation(sourceText, targetText, combinedAnalysis); |
| |
| |
| setTimeout(() => { |
| document.querySelectorAll('.highlight-number, .highlight-meaning, .highlight-missing, .completely-missing, .partially-missing').forEach(element => { |
| element.addEventListener('click', function() { |
| const errorType = this.getAttribute('data-error-type') || |
| (this.classList.contains('completely-missing') ? 'missing' : |
| this.classList.contains('partially-missing') ? 'missing' : 'number'); |
| const errorText = this.textContent; |
| let explanation = ''; |
| |
| if (this.classList.contains('completely-missing')) { |
| explanation = 'هذا النص مفقود تماما في الترجمة. يجب إضافته للحفاظ على اكتمال المحتوى.'; |
| } else if (this.classList.contains('partially-missing')) { |
| explanation = 'هذا النص موجود بشكل جزئي في الترجمة. تأكد من ترجمة كل المحتوى.'; |
| } else { |
| explanation = decodeURIComponent(this.getAttribute('data-explanation') || 'لا يوجد شرح متاح'); |
| } |
| |
| showErrorExplanation(errorType, errorText, explanation); |
| }); |
| }); |
| }, 100); |
| } |
| |
| |
| |
| |
| 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); |
| |
| |
| 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); |
| }); |
| |
| |
| setTimeout(() => { |
| document.querySelectorAll('.segment-source .highlight-number, .segment-source .highlight-meaning, .segment-target .highlight-missing, .segment-source .completely-missing, .segment-source .partially-missing').forEach(element => { |
| element.addEventListener('click', function() { |
| const errorType = this.getAttribute('data-error-type') || |
| (this.classList.contains('completely-missing') ? 'missing' : |
| this.classList.contains('partially-missing') ? 'missing' : 'number'); |
| const errorText = this.textContent; |
| let explanation = ''; |
| |
| if (this.classList.contains('completely-missing')) { |
| explanation = 'هذا النص مفقود تماما في الترجمة. يجب إضافته للحفاظ على اكتمال المحتوى.'; |
| } else if (this.classList.contains('partially-missing')) { |
| explanation = 'هذا النص موجود بشكل جزئي في الترجمة. تأكد من ترجمة كل المحتوى.'; |
| } else { |
| explanation = decodeURIComponent(this.getAttribute('data-explanation') || 'لا يوجد شرح متاح'); |
| } |
| |
| showErrorExplanation(errorType, errorText, explanation); |
| }); |
| }); |
| }, 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 = ` |
| <p class="mb-2">استخدم الأزرار "التالي" و"السابق" للتنقل بين الاختلافات المختلفة التي تم العثور عليها.</p> |
| <p>يمكنك تصفية الاختلافات حسب نوعها باستخدام خيارات التصفية الموجودة أعلاه.</p> |
| `; |
| } |
| |
| |
| 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 showNextDifference() { |
| if (allDifferences.length === 0) return; |
| |
| currentDiffIndex = (currentDiffIndex + 1) % allDifferences.length; |
| displayCurrentDifference(); |
| } |
| |
| |
| function showPreviousDifference() { |
| if (allDifferences.length === 0) return; |
| |
| currentDiffIndex = (currentDiffIndex - 1 + allDifferences.length) % 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 { |
| |
| 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 ''; |
| } |
| |
| |
| |
| |
| |
| function generateExplanation(sourceText, targetText, analysisOutput) { |
| |
| let steps = []; |
| |
| |
| const completelyMissing = []; |
| const sourceParagraphs = sourceText.split(/\n\s*\n/).filter(p => p.trim()); |
| |
| for (const paragraph of sourceParagraphs) { |
| if (paragraph.trim().length < 10) continue; |
| |
| |
| if (!targetText.includes(paragraph) && !isPartiallyIncluded(paragraph, targetText)) { |
| completelyMissing.push({ |
| text: truncateText(paragraph, 100), |
| lineNum: getLineNumber(sourceText, paragraph) |
| }); |
| } |
| } |
| |
| |
| if (completelyMissing.length > 0) { |
| completelyMissing.forEach(missing => { |
| steps.push(` |
| <li class="mb-3 pb-3 border-b border-gray-200"> |
| <div class="font-bold text-blue-700 mb-2 flex items-center"> |
| <i class="fas fa-minus-circle text-blue-500 ml-2"></i> |
| <span>النص المفقود تماما:</span> |
| <span class="mr-2 bg-blue-100 text-blue-800 text-sm px-2 py-1 rounded">السطر ${missing.lineNum}</span> |
| </div> |
| <div class="bg-blue-50 p-3 rounded-lg border-r-4 border-blue-400"> |
| ${missing.text} |
| </div> |
| <div class="mt-2 text-sm text-gray-600"> |
| <i class="fas fa-info-circle ml-1"></i> |
| هذا النص موجود في المصدر ومفقود تماما في الترجمة. |
| </div> |
| </li>`); |
| }); |
| } |
| |
| |
| const numberRegex = /<([^<>]+)>/g; |
| let match; |
| while ((match = numberRegex.exec(analysisOutput)) !== null) { |
| const phrase = match[1].trim(); |
| if (phrase) { |
| const lineNum = getLineNumber(sourceText, phrase); |
| steps.push(` |
| <li class="mb-3 pb-3 border-b border-gray-200"> |
| <div class="font-bold text-yellow-700 mb-2 flex items-center"> |
| <i class="fas fa-hashtag text-yellow-500 ml-2"></i> |
| <span>اختلاف رقمي:</span> |
| <span class="mr-2 bg-yellow-100 text-yellow-800 text-sm px-2 py-1 rounded">السطر ${lineNum}</span> |
| </div> |
| <div class="bg-yellow-50 p-3 rounded-lg border-r-4 border-yellow-400"> |
| ${phrase} |
| </div> |
| <div class="mt-2 text-sm text-gray-600"> |
| <i class="fas fa-info-circle ml-1"></i> |
| تأكد من تطابق هذا الرقم في النص الهدف. |
| </div> |
| </li>`); |
| } |
| } |
| |
| |
| const missingRegex = /__(.*?)__/g; |
| while ((match = missingRegex.exec(analysisOutput)) !== null) { |
| const phrase = match[1].trim(); |
| if (phrase) { |
| const lineNum = getLineNumber(sourceText, phrase); |
| steps.push(` |
| <li class="mb-3 pb-3 border-b border-gray-200"> |
| <div class="font-bold text-blue-700 mb-2 flex items-center"> |
| <i class="fas fa-minus-circle text-blue-500 ml-2"></i> |
| <span>نص مفقود:</span> |
| <span class="mr-2 bg-blue-100 text-blue-800 text-sm px-2 py-1 rounded">السطر ${lineNum}</span> |
| </div> |
| <div class="bg-blue-50 p-3 rounded-lg border-r-4 border-blue-400"> |
| ${phrase} |
| </div> |
| <div class="mt-2 text-sm text-gray-600"> |
| <i class="fas fa-info-circle ml-1"></i> |
| تأكد من وجود هذا النص في الترجمة. |
| </div> |
| </li>`); |
| } |
| } |
| |
| |
| const meaningRegex = /\[MEANING\](.*?)\[\/MEANING\]/g; |
| while ((match = meaningRegex.exec(analysisOutput)) !== null) { |
| const phrase = match[1].trim(); |
| if (phrase) { |
| const lineNum = getLineNumber(sourceText, phrase); |
| steps.push(` |
| <li class="mb-3 pb-3 border-b border-gray-200"> |
| <div class="font-bold text-red-700 mb-2 flex items-center"> |
| <i class="fas fa-exclamation-circle text-red-500 ml-2"></i> |
| <span>اختلاف معنى:</span> |
| <span class="mr-2 bg-red-100 text-red-800 text-sm px-2 py-1 rounded">السطر ${lineNum}</span> |
| </div> |
| <div class="bg-red-50 p-3 rounded-lg border-r-4 border-red-400"> |
| ${phrase} |
| </div> |
| <div class="mt-2 text-sm text-gray-600"> |
| <i class="fas fa-info-circle ml-1"></i> |
| راجع ترجمة هذا النص للتأكد من نقل المعنى الصحيح. |
| </div> |
| </li>`); |
| } |
| } |
| |
| |
| if (steps.length === 0) { |
| return ` |
| <div class="flex flex-col items-center justify-center p-6 bg-green-50 rounded-xl border border-green-200"> |
| <div class="text-5xl text-green-500 mb-3"> |
| <i class="fas fa-check-circle"></i> |
| </div> |
| <p class="text-lg font-bold text-green-700">النصوص متطابقة تماماً ولا توجد فروقات تحتاج للتنبيه.</p> |
| <p class="text-sm text-green-600 mt-2">تهانينا! الترجمة دقيقة ومكتملة.</p> |
| </div>`; |
| } |
| |
| return ` |
| <div class="bg-gray-50 p-4 rounded-xl border border-gray-200 mb-4"> |
| <h4 class="text-lg font-bold text-gray-700 mb-3 flex items-center"> |
| <i class="fas fa-clipboard-check text-blue-600 ml-2"></i> |
| ملخص الاختلافات |
| </h4> |
| <p class="text-gray-600">تم العثور على ${steps.length} اختلاف يحتاج إلى مراجعة.</p> |
| </div> |
| <ol class="list-none space-y-4">${steps.join('')}</ol>`; |
| } |
| |
| |
| 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++; |
| } |
| } |
| |
| |
| return keywords.length > 0 && foundKeywords / keywords.length > 0.3; |
| } |
| |
| |
| |
| |
| document.getElementById('toggleDraftBtn').addEventListener('click', function() { |
| const draftSection = document.getElementById('fullTextDraftSection'); |
| draftSection.classList.toggle('hidden'); |
| |
| |
| const isHidden = draftSection.classList.contains('hidden'); |
| this.innerHTML = isHidden ? |
| '<i class="fas fa-eye ml-2"></i> عرض مسودة التحليل' : |
| '<i class="fas fa-eye-slash ml-2"></i> إخفاء مسودة التحليل'; |
| }); |
| |
| |
| |
| |
| document.getElementById('downloadExcelBtn').addEventListener('click', function() { |
| |
| 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("حدث خطأ أثناء إنشاء التقرير"); |
| } |
| }); |
| |
| |
| |
| |
| document.getElementById('downloadWordBtn').addEventListener('click', function() { |
| alert('سيتم تنفيذ تنزيل التقرير بصيغة Word قريبًا'); |
| }); |
| |
| |
| |
| |
| function processFileForOCR(type) { |
| const fileInput = type === 'source' ? document.getElementById('sourceFile') : document.getElementById('targetFile'); |
| const file = fileInput.files[0]; |
| |
| if (!file) { |
| alert('الرجاء اختيار ملف أولاً'); |
| return; |
| } |
| |
| if (file.type === 'application/pdf') { |
| processPDF(file, type); |
| } else if (file.type.startsWith('image/')) { |
| processImage(file, type); |
| } else { |
| alert('يرجى اختيار ملف PDF أو صورة للمعالجة'); |
| } |
| } |
| |
| async function processPDF(file, type) { |
| try { |
| currentProcessingMode = type; |
| document.getElementById('processingStatus').classList.remove('hidden'); |
| document.getElementById('statusText').textContent = 'جاري معالجة ملف PDF...'; |
| document.getElementById('progressBar').style.width = '20%'; |
| |
| const arrayBuffer = await file.arrayBuffer(); |
| const loadingTask = pdfjsLib.getDocument(arrayBuffer); |
| const pdf = await loadingTask.promise; |
| |
| documentPages = []; |
| selectedPages = []; |
| |
| document.getElementById('progressBar').style.width = '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; |
| |
| await page.render({ |
| canvasContext: context, |
| viewport: viewport |
| }).promise; |
| |
| documentPages.push({ |
| pageNum: i, |
| imageData: canvas.toDataURL('image/jpeg'), |
| selected: false |
| }); |
| } |
| |
| document.getElementById('progressBar').style.width = '100%'; |
| document.getElementById('pdfPagesCard').classList.remove('hidden'); |
| document.getElementById('resultsCard').classList.add('hidden'); |
| |
| displayPDFPages(); |
| |
| } catch (error) { |
| console.error('خطأ في معالجة ملف PDF:', error); |
| alert('حدث خطأ أثناء معالجة ملف PDF'); |
| document.getElementById('processingStatus').classList.add('hidden'); |
| } |
| } |
| |
| function processImage(file, type) { |
| currentProcessingMode = type; |
| document.getElementById('processingStatus').classList.remove('hidden'); |
| document.getElementById('statusText').textContent = 'جاري معالجة الصورة...'; |
| document.getElementById('progressBar').style.width = '30%'; |
| |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| const img = new Image(); |
| img.onload = function() { |
| documentPages = [{ |
| pageNum: 1, |
| imageData: e.target.result, |
| selected: true |
| }]; |
| selectedPages = [0]; |
| |
| document.getElementById('progressBar').style.width = '100%'; |
| document.getElementById('pdfPagesCard').classList.remove('hidden'); |
| document.getElementById('resultsCard').classList.add('hidden'); |
| |
| displayPDFPages(); |
| }; |
| img.src = e.target.result; |
| }; |
| reader.onerror = function() { |
| console.error('خطأ في قراءة الصورة'); |
| alert('حدث خطأ أثناء قراءة الصورة'); |
| document.getElementById('processingStatus').classList.add('hidden'); |
| }; |
| reader.readAsDataURL(file); |
| } |
| |
| 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'); |
| } |
| |
| 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%'; |
| |
| 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) * 100}%`; |
| |
| |
| const base64Image = pageData.imageData.split(',')[1]; |
| |
| |
| const formData = new FormData(); |
| formData.append('image', base64Image); |
| formData.append('language', 'ara'); |
| |
| const response = await fetch(OCR_API_URL, { |
| method: 'POST', |
| headers: { |
| 'x-rapidapi-key': RAPIDAPI_KEY, |
| 'x-rapidapi-host': 'ocr43.p.rapidapi.com' |
| }, |
| body: formData |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`خطأ في استخراج النص: ${response.statusText}`); |
| } |
| |
| const data = await response.json(); |
| |
| if (data && data.text) { |
| extractedTexts.push(data.text); |
| extractedPageNumbers.push(pageData.pageNum); |
| 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 = '100%'; |
| |
| |
| displayExtractedTexts(); |
| |
| } catch (error) { |
| console.error('خطأ في استخراج النص:', error); |
| alert('حدث خطأ أثناء استخراج النص: ' + error.message); |
| document.getElementById('processingStatus').classList.add('hidden'); |
| } |
| } |
| |
| 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]}</h4> |
| <div>${text.substring(0, 100)}${text.length > 100 ? '...' : ''}</div> |
| `; |
| |
| resultPreview.appendChild(pagePreview); |
| }); |
| |
| |
| document.getElementById('resultText').textContent = extractedTexts.join('\n\n'); |
| } |
| |
| function copyText() { |
| const resultText = document.getElementById('resultText'); |
| |
| |
| 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; |
| } |
| |
| alert(`تم استخدام النص المستخرج كنص ${target === 'source' ? 'مصدر' : 'هدف'}`); |
| } |
| |
| |
| |
| |
| function initializeImageEditor(index) { |
| currentPageIndex = index; |
| |
| const imageCanvas = document.getElementById('imageCanvas'); |
| const imageEditor = document.getElementById('imageEditor'); |
| |
| imageEditor.classList.remove('hidden'); |
| |
| |
| originalImageData = documentPages[index].imageData; |
| |
| |
| 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) { |
| |
| 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(); |
| } |
| } |
| </script> |
| </body> |
| </html> |