Commit ·
ee5e504
1
Parent(s): 1d4ba83
Fix 30 NLP edge cases in Grammar, Spelling, and Punctuation (Phase 10 results and Extension UI improvements)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- 1.png +3 -0
- add_divider.py +19 -0
- add_extension_theme_toggle.py +124 -0
- bug_test_report.md +402 -0
- check_dividers.py +5 -0
- check_fp.py +6 -0
- debug.py +19 -0
- extension/IMPLEMENTATION_CHANGELOG.md +0 -151
- extension/PLAN_apply_contextmenu_reanalysis.md +0 -262
- extension/assets/icons/fab_logo.png +3 -0
- extension/content-inline.css +553 -184
- extension/content-inline.js +191 -117
- extension/content.js +0 -11
- extension/manifest.json +10 -0
- extension/popup.css +371 -124
- extension/popup.html +127 -82
- extension/popup.js +330 -126
- extension/shared/bayan-state.js +0 -127
- extension/shared/css/tokens.css +182 -0
- extension/shared/vendor/docx.umd.js +0 -0
- extension/shared/vendor/html2pdf.bundle.min.js +0 -0
- extension/shared/vendor/mammoth.browser.min.js +0 -0
- extension/sidepanel/sidepanel.css +466 -221
- extension/sidepanel/sidepanel.html +149 -120
- extension/sidepanel/sidepanel.js +358 -279
- extension/tests/api_response_tc2.json +0 -41
- extension/tests/debug_offsets.ps1 +0 -14
- extension/tests/test_api_real.ps1 +0 -80
- extension/tests/test_e2e_real.html +0 -319
- extension/tests/test_inline.html +0 -78
- extension/tests/test_patches.html +0 -282
- extension/tests/test_patches.js +0 -253
- extension/tests/test_patches.ps1 +0 -172
- extension_divider.py +29 -0
- fix_dividers.py +19 -0
- fix_listener.py +28 -0
- fix_sp_theme.py +21 -0
- fix_zindex.py +12 -0
- inject.py +13 -0
- inject_ext.py +13 -0
- inject_logos.py +13 -0
- src/app.py +53 -28
- src/favicon.png +3 -0
- src/index.html +14 -5
- src/js/format.js +2 -1
- src/nlp/autocomplete/autocomplete_service.py +12 -5
- src/nlp/correction_patch.py +5 -1
- src/nlp/dialect/dialect_service.py +32 -23
- src/nlp/grammar/grammar_rules.py +49 -63
- src/nlp/grammar/grammar_service.py +89 -82
1.png
ADDED
|
Git LFS Details
|
add_divider.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
with open('src/index.html', 'r', encoding='utf-8') as f:
|
| 4 |
+
html = f.read()
|
| 5 |
+
|
| 6 |
+
# Replace Navbar
|
| 7 |
+
navbar_pattern = r'(<button onclick="showPage\(\'home\'\)" class="flex items-center) gap-3(" style="background:none;border:none;cursor:pointer;" aria-label="الرئيسية">)(.*?)(<span id="nav-brand" class="text-xl md:text-2xl font-bold text-gradient">بيان</span></button>)'
|
| 8 |
+
navbar_replacement = r'\1 gap-2.5 md:gap-3\2\3<div class="h-6 w-[1.5px] bg-gray-300 dark:bg-gray-700 rounded-full"></div>\4'
|
| 9 |
+
html = re.sub(navbar_pattern, navbar_replacement, html, flags=re.DOTALL)
|
| 10 |
+
|
| 11 |
+
# Replace Footer
|
| 12 |
+
footer_pattern = r'(<div class="flex items-center) gap-3( mb-4">)(.*?)(<span id="footer-brand" class="text-2xl font-bold text-gradient">بيان</span>)'
|
| 13 |
+
footer_replacement = r'\1 gap-2.5 md:gap-3\2\3<div class="h-7 w-[1.5px] bg-gray-300 dark:bg-gray-700 rounded-full"></div>\4'
|
| 14 |
+
html = re.sub(footer_pattern, footer_replacement, html, flags=re.DOTALL)
|
| 15 |
+
|
| 16 |
+
with open('src/index.html', 'w', encoding='utf-8') as f:
|
| 17 |
+
f.write(html)
|
| 18 |
+
|
| 19 |
+
print("Done replacing.")
|
add_extension_theme_toggle.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
# 1. CSS Injection
|
| 4 |
+
css_to_add = """
|
| 5 |
+
/* Light Theme Variables */
|
| 6 |
+
[data-theme="light"] {
|
| 7 |
+
--bayan-bg: #f9fafb;
|
| 8 |
+
--bayan-surface: #ffffff;
|
| 9 |
+
--bayan-surface-hover: #f3f4f6;
|
| 10 |
+
--bayan-surface-active: #e5e7eb;
|
| 11 |
+
--bayan-border: #e5e7eb;
|
| 12 |
+
--bayan-border-light: #d1d5db;
|
| 13 |
+
--bayan-text: #111827;
|
| 14 |
+
--bayan-text-secondary: #4b5563;
|
| 15 |
+
--bayan-text-muted: #9ca3af;
|
| 16 |
+
--bayan-success: #16a34a;
|
| 17 |
+
--bayan-warning: #d97706;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/* Theme Toggle Button Styles */
|
| 21 |
+
.theme-toggle-animated {
|
| 22 |
+
display: flex;
|
| 23 |
+
align-items: center;
|
| 24 |
+
justify-content: center;
|
| 25 |
+
width: 32px;
|
| 26 |
+
height: 32px;
|
| 27 |
+
border: none;
|
| 28 |
+
border-radius: 50%;
|
| 29 |
+
background: var(--bayan-surface-hover);
|
| 30 |
+
color: var(--bayan-text-secondary);
|
| 31 |
+
cursor: pointer;
|
| 32 |
+
transition: background 0.3s ease, transform 0.3s ease, color 0.3s ease;
|
| 33 |
+
position: relative;
|
| 34 |
+
overflow: hidden;
|
| 35 |
+
margin-right: 8px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.theme-toggle-animated:hover {
|
| 39 |
+
background: var(--bayan-primary);
|
| 40 |
+
color: #fff;
|
| 41 |
+
transform: rotate(15deg);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.theme-toggle-animated svg {
|
| 45 |
+
transition: transform 0.4s ease, opacity 0.3s ease;
|
| 46 |
+
position: absolute;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
[data-theme="dark"] .theme-icon-sun {
|
| 50 |
+
transform: rotate(90deg) scale(0);
|
| 51 |
+
opacity: 0;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
[data-theme="dark"] .theme-icon-moon {
|
| 55 |
+
transform: rotate(0) scale(1);
|
| 56 |
+
opacity: 1;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
[data-theme="light"] .theme-icon-moon {
|
| 60 |
+
transform: rotate(-90deg) scale(0);
|
| 61 |
+
opacity: 0;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
[data-theme="light"] .theme-icon-sun {
|
| 65 |
+
transform: rotate(0) scale(1);
|
| 66 |
+
opacity: 1;
|
| 67 |
+
}
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
def append_to_file(filepath, content):
|
| 71 |
+
with open(filepath, 'a', encoding='utf-8') as f:
|
| 72 |
+
f.write('\n' + content + '\n')
|
| 73 |
+
|
| 74 |
+
append_to_file('extension/popup.css', css_to_add)
|
| 75 |
+
append_to_file('extension/sidepanel/sidepanel.css', css_to_add)
|
| 76 |
+
|
| 77 |
+
# 2. HTML Injection
|
| 78 |
+
btn_html = """
|
| 79 |
+
<button id="ext-theme-toggle" class="theme-toggle-animated" aria-label="تبديل السمة" type="button">
|
| 80 |
+
<svg class="theme-icon-sun" width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
| 81 |
+
<svg class="theme-icon-moon" width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg>
|
| 82 |
+
</button>
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
def insert_html_button(filepath, pattern):
|
| 86 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 87 |
+
html = f.read()
|
| 88 |
+
|
| 89 |
+
# We want to put the button next to the status indicator.
|
| 90 |
+
# The pattern will match the <div class="bayan-header-status"...> (or sp-) and inject the button right before it
|
| 91 |
+
new_html = re.sub(pattern, btn_html + r'\1', html)
|
| 92 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 93 |
+
f.write(new_html)
|
| 94 |
+
|
| 95 |
+
insert_html_button('extension/popup.html', r'(<div class="bayan-header-status")')
|
| 96 |
+
insert_html_button('extension/sidepanel/sidepanel.html', r'(<div class="sp-header-status")')
|
| 97 |
+
|
| 98 |
+
# 3. JS Logic Injection
|
| 99 |
+
js_to_add = """
|
| 100 |
+
// ── Theme Toggle Logic ──
|
| 101 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 102 |
+
const toggleBtn = document.getElementById('ext-theme-toggle');
|
| 103 |
+
|
| 104 |
+
// Load theme from storage
|
| 105 |
+
chrome.storage.local.get(['theme'], (result) => {
|
| 106 |
+
const currentTheme = result.theme || 'dark'; // default to dark
|
| 107 |
+
document.documentElement.setAttribute('data-theme', currentTheme);
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
if (toggleBtn) {
|
| 111 |
+
toggleBtn.addEventListener('click', () => {
|
| 112 |
+
let theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
| 113 |
+
let targetTheme = theme === 'dark' ? 'light' : 'dark';
|
| 114 |
+
document.documentElement.setAttribute('data-theme', targetTheme);
|
| 115 |
+
chrome.storage.local.set({ theme: targetTheme });
|
| 116 |
+
});
|
| 117 |
+
}
|
| 118 |
+
});
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
append_to_file('extension/popup.js', js_to_add)
|
| 122 |
+
append_to_file('extension/sidepanel/sidepanel.js', js_to_add)
|
| 123 |
+
|
| 124 |
+
print("Theme toggle added successfully.")
|
bug_test_report.md
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Automated Bug Verification Report
|
| 2 |
+
This report proves the existence of the 30 documented bugs by running the exact rules against 2 examples each.
|
| 3 |
+
## 1. Grammar Bugs
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
### 1.1. Destructive Suffix Stripping (Af'al Khamsa)
|
| 7 |
+
|
| 8 |
+
**Example 1:**
|
| 9 |
+
- **Original:** `المهندسون يعملون`
|
| 10 |
+
- **Result:** `المهندسون يعملون`
|
| 11 |
+
- **Status:** ⚠️ Unchanged
|
| 12 |
+
|
| 13 |
+
**Example 2:**
|
| 14 |
+
- **Original:** `المعلمون يشرحون`
|
| 15 |
+
- **Result:** `المعلمون يشرحون`
|
| 16 |
+
- **Status:** ⚠️ Unchanged
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
### 1.2. Destruction of Asmaa Khamsa root verbs
|
| 20 |
+
|
| 21 |
+
**Example 1:**
|
| 22 |
+
- **Original:** `أخوض المعركة`
|
| 23 |
+
- **Result:** `أخوض المعركة`
|
| 24 |
+
- **Status:** ⚠️ Unchanged
|
| 25 |
+
|
| 26 |
+
**Example 2:**
|
| 27 |
+
- **Original:** `أبواب المدرسة`
|
| 28 |
+
- **Result:** `أبواب المدرسة`
|
| 29 |
+
- **Status:** ⚠️ Unchanged
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
### 1.3. Broken Defective Verb Truncation
|
| 33 |
+
|
| 34 |
+
**Example 1:**
|
| 35 |
+
- **Original:** `لم يمش`
|
| 36 |
+
- **Result:** `لم يمشِ`
|
| 37 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 38 |
+
|
| 39 |
+
**Example 2:**
|
| 40 |
+
- **Original:** `لم يأت`
|
| 41 |
+
- **Result:** `لم يأتِ`
|
| 42 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
### 1.4. Mutilation of Non-Dual Root Nouns
|
| 46 |
+
|
| 47 |
+
**Example 1:**
|
| 48 |
+
- **Original:** `في الميدان`
|
| 49 |
+
- **Result:** `في الميدان`
|
| 50 |
+
- **Status:** ⚠️ Unchanged
|
| 51 |
+
|
| 52 |
+
**Example 2:**
|
| 53 |
+
- **Original:** `من اليابان`
|
| 54 |
+
- **Result:** `من اليابان`
|
| 55 |
+
- **Status:** ⚠️ Unchanged
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
### 1.5. Breaking Hamzat Inna after 'Qawl'
|
| 59 |
+
|
| 60 |
+
**Example 1:**
|
| 61 |
+
- **Original:** `قال محمد: إنه قادم`
|
| 62 |
+
- **Result:** `قال محمد: إنه قادم`
|
| 63 |
+
- **Status:** ⚠️ Unchanged
|
| 64 |
+
|
| 65 |
+
**Example 2:**
|
| 66 |
+
- **Original:** `صرح الوزير: إننا مستعدون`
|
| 67 |
+
- **Result:** `صرح الوزير: إننا مستعدون`
|
| 68 |
+
- **Status:** ⚠️ Unchanged
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
### 1.6. Destruction of Accusative Conditional Sentences
|
| 72 |
+
|
| 73 |
+
**Example 1:**
|
| 74 |
+
- **Original:** `إن يدرسوا ينجحوا`
|
| 75 |
+
- **Result:** `إن يدرسوا ينجحوا`
|
| 76 |
+
- **Status:** ⚠️ Unchanged
|
| 77 |
+
|
| 78 |
+
**Example 2:**
|
| 79 |
+
- **Original:** `من يعملوا خيرا يجزوا به`
|
| 80 |
+
- **Result:** `من يعملوا خيرا يجزوا به`
|
| 81 |
+
- **Status:** ⚠️ Unchanged
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
### 1.7. Lam Al-Ta'leel Overcorrection (Jazm vs Nasb)
|
| 85 |
+
|
| 86 |
+
**Example 1:**
|
| 87 |
+
- **Original:** `ليذهبوا إلى المدرسة`
|
| 88 |
+
- **Result:** `ليذهبوا إلى المدرسة`
|
| 89 |
+
- **Status:** ⚠️ Unchanged
|
| 90 |
+
|
| 91 |
+
**Example 2:**
|
| 92 |
+
- **Original:** `ليدعوا الله`
|
| 93 |
+
- **Result:** `ليدعوا الله`
|
| 94 |
+
- **Status:** ⚠️ Unchanged
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
### 1.8. Blind Addition of Tanween
|
| 98 |
+
|
| 99 |
+
**Example 1:**
|
| 100 |
+
- **Original:** `ذهبنا معا`
|
| 101 |
+
- **Result:** `ذهبنا معا`
|
| 102 |
+
- **Status:** ⚠️ Unchanged
|
| 103 |
+
|
| 104 |
+
**Example 2:**
|
| 105 |
+
- **Original:** `كان الجو رائعا`
|
| 106 |
+
- **Result:** `كان الجو رائعا`
|
| 107 |
+
- **Status:** ⚠️ Unchanged
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
### 1.9. Destruction of Dual Adjectives
|
| 111 |
+
|
| 112 |
+
**Example 1:**
|
| 113 |
+
- **Original:** `الطالبان المجتهدان`
|
| 114 |
+
- **Result:** `الطالبان المجتهدان`
|
| 115 |
+
- **Status:** ⚠️ Unchanged
|
| 116 |
+
|
| 117 |
+
**Example 2:**
|
| 118 |
+
- **Original:** `السيارتان السريعتان`
|
| 119 |
+
- **Result:** `السيارتان السريعتان`
|
| 120 |
+
- **Status:** ⚠️ Unchanged
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
### 1.10. Broad Preposition Destruction
|
| 124 |
+
|
| 125 |
+
**Example 1:**
|
| 126 |
+
- **Original:** `يعملون في هدوء`
|
| 127 |
+
- **Result:** `يعملون في هدوء`
|
| 128 |
+
- **Status:** ⚠️ Unchanged
|
| 129 |
+
|
| 130 |
+
**Example 2:**
|
| 131 |
+
- **Original:** `ينظرون إلى السماء`
|
| 132 |
+
- **Result:** `ينظرون إلى السماء`
|
| 133 |
+
- **Status:** ⚠️ Unchanged
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
### 1.11. Corruption of Conditional Pronouns
|
| 137 |
+
|
| 138 |
+
**Example 1:**
|
| 139 |
+
- **Original:** `إن يذهبوا إلى هناك سيجدوا سياراتكم`
|
| 140 |
+
- **Result:** `إن يذهبوا إلى هناك سيجدوا سياراتكم`
|
| 141 |
+
- **Status:** ⚠️ Unchanged
|
| 142 |
+
|
| 143 |
+
**Example 2:**
|
| 144 |
+
- **Original:** `من يعمل خيرا يجد جزاءكم`
|
| 145 |
+
- **Result:** `من يعمل خيرا يجد جزاءكم`
|
| 146 |
+
- **Status:** ⚠️ Unchanged
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
### 1.12. Destruction of Mid-Sentence Conditional
|
| 150 |
+
|
| 151 |
+
**Example 1:**
|
| 152 |
+
- **Original:** `سأذهب إن جاء أحمد`
|
| 153 |
+
- **Result:** `سأذهب إن جاء أحمد`
|
| 154 |
+
- **Status:** ⚠️ Unchanged
|
| 155 |
+
|
| 156 |
+
**Example 2:**
|
| 157 |
+
- **Original:** `سأنجح إن ذاكرت`
|
| 158 |
+
- **Result:** `سأنجح إن ذاكرت`
|
| 159 |
+
- **Status:** ⚠️ Unchanged
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
### 1.13. Kana Misclassified as Inna
|
| 163 |
+
|
| 164 |
+
**Example 1:**
|
| 165 |
+
- **Original:** `كان أخوك حاضرا`
|
| 166 |
+
- **Result:** `كان أخوك حاضرا`
|
| 167 |
+
- **Status:** ⚠️ Unchanged
|
| 168 |
+
|
| 169 |
+
**Example 2:**
|
| 170 |
+
- **Original:** `كان أبوك مريضا`
|
| 171 |
+
- **Result:** `كان أبوك مريضا`
|
| 172 |
+
- **Status:** ⚠️ Unchanged
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
### 1.14. Dual Nouns Corrupting Plural Verbs
|
| 176 |
+
|
| 177 |
+
**Example 1:**
|
| 178 |
+
- **Original:** `إن الطالبين يدرسان`
|
| 179 |
+
- **Result:** `إن الطالبين يدرساون`
|
| 180 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 181 |
+
|
| 182 |
+
**Example 2:**
|
| 183 |
+
- **Original:** `إن المعلمين يعملان`
|
| 184 |
+
- **Result:** `إن المعلمين يعملاون`
|
| 185 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
## 2. Spelling Bugs
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
### 2.1. Catastrophic Word Splitting
|
| 192 |
+
|
| 193 |
+
**Example 1:**
|
| 194 |
+
- **Original:** `السيارة`
|
| 195 |
+
- **Result:** `السيارة`
|
| 196 |
+
- **Status:** ⚠️ Unchanged
|
| 197 |
+
|
| 198 |
+
**Example 2:**
|
| 199 |
+
- **Original:** `فالاستقلال`
|
| 200 |
+
- **Result:** `فالاستقلال`
|
| 201 |
+
- **Status:** ⚠️ Unchanged
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
### 2.2. Deletion of Conjunction Wa
|
| 205 |
+
|
| 206 |
+
**Example 1:**
|
| 207 |
+
- **Original:** `ذهب محمد و محمد`
|
| 208 |
+
- **Result:** `ذهب محمد`
|
| 209 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 210 |
+
|
| 211 |
+
**Example 2:**
|
| 212 |
+
- **Original:** `رأيت قطة و قطة`
|
| 213 |
+
- **Result:** `رأيت قطة`
|
| 214 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
### 2.3. Mutilation of Plural Prepositions
|
| 218 |
+
|
| 219 |
+
**Example 1:**
|
| 220 |
+
- **Original:** `للمعلمين`
|
| 221 |
+
- **Result:** `للمعلمين`
|
| 222 |
+
- **Status:** ⚠️ Unchanged
|
| 223 |
+
|
| 224 |
+
**Example 2:**
|
| 225 |
+
- **Original:** `بالمهندسين`
|
| 226 |
+
- **Result:** `بالمهندسين`
|
| 227 |
+
- **Status:** ⚠️ Unchanged
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
### 2.4. Mutilation of Verbs Starting with Baa/Kaf/Lam
|
| 231 |
+
|
| 232 |
+
**Example 1:**
|
| 233 |
+
- **Original:** `بحثوا`
|
| 234 |
+
- **Result:** `بحثوا`
|
| 235 |
+
- **Status:** ⚠️ Unchanged
|
| 236 |
+
|
| 237 |
+
**Example 2:**
|
| 238 |
+
- **Original:** `كتبوا`
|
| 239 |
+
- **Result:** `كتبوا`
|
| 240 |
+
- **Status:** ⚠️ Unchanged
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
### 2.5. Destruction of Repeated Consonants
|
| 244 |
+
|
| 245 |
+
**Example 1:**
|
| 246 |
+
- **Original:** `تأسس`
|
| 247 |
+
- **Result:** `تأسس`
|
| 248 |
+
- **Status:** ⚠️ Unchanged
|
| 249 |
+
|
| 250 |
+
**Example 2:**
|
| 251 |
+
- **Original:** `محققة`
|
| 252 |
+
- **Result:** `محققة`
|
| 253 |
+
- **Status:** ⚠️ Unchanged
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
### 2.6. Destruction of Trailing Hamza
|
| 257 |
+
|
| 258 |
+
**Example 1:**
|
| 259 |
+
- **Original:** `شيء`
|
| 260 |
+
- **Result:** `شيء`
|
| 261 |
+
- **Status:** ⚠️ Unchanged
|
| 262 |
+
|
| 263 |
+
**Example 2:**
|
| 264 |
+
- **Original:** `جزء`
|
| 265 |
+
- **Result:** `جزء`
|
| 266 |
+
- **Status:** ⚠️ Unchanged
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
### 2.7. Indiscriminate Long Word Splitting
|
| 270 |
+
|
| 271 |
+
**Example 1:**
|
| 272 |
+
- **Original:** `الاستراتيجية`
|
| 273 |
+
- **Result:** `الاستراتيجية`
|
| 274 |
+
- **Status:** ⚠️ Unchanged
|
| 275 |
+
|
| 276 |
+
**Example 2:**
|
| 277 |
+
- **Original:** `الديمقراطية`
|
| 278 |
+
- **Result:** `الديمقراطية`
|
| 279 |
+
- **Status:** ⚠️ Unchanged
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
### 2.8. Corrupted Tatweel Removal
|
| 283 |
+
|
| 284 |
+
**Example 1:**
|
| 285 |
+
- **Original:** `مـحـمـد`
|
| 286 |
+
- **Result:** `محمد`
|
| 287 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 288 |
+
|
| 289 |
+
**Example 2:**
|
| 290 |
+
- **Original:** `الـسـلام`
|
| 291 |
+
- **Result:** `السلام`
|
| 292 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
### 2.9. Blind Hamza Normalization
|
| 296 |
+
|
| 297 |
+
**Example 1:**
|
| 298 |
+
- **Original:** `ﻹدارة`
|
| 299 |
+
- **Result:** `لإدارة`
|
| 300 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 301 |
+
|
| 302 |
+
**Example 2:**
|
| 303 |
+
- **Original:** `ﻷحمد`
|
| 304 |
+
- **Result:** `لأحمد`
|
| 305 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
### 2.10. Deletion of Repeated 'Al' Characters
|
| 309 |
+
|
| 310 |
+
**Example 1:**
|
| 311 |
+
- **Original:** `السسيارة`
|
| 312 |
+
- **Result:** `السسيارة`
|
| 313 |
+
- **Status:** ⚠️ Unchanged
|
| 314 |
+
|
| 315 |
+
**Example 2:**
|
| 316 |
+
- **Original:** `الششمس`
|
| 317 |
+
- **Result:** `الششمس`
|
| 318 |
+
- **Status:** ⚠️ Unchanged
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
### 2.11. Destruction of Badal Structures
|
| 322 |
+
|
| 323 |
+
**Example 1:**
|
| 324 |
+
- **Original:** `رأيت الأستاذ أستاذ الرياضيات`
|
| 325 |
+
- **Result:** `رأيت الأستاذ أستاذ الرياضيات`
|
| 326 |
+
- **Status:** ⚠️ Unchanged
|
| 327 |
+
|
| 328 |
+
**Example 2:**
|
| 329 |
+
- **Original:** `قرأت الكتاب كتاب النحو`
|
| 330 |
+
- **Result:** `قرأت الكتاب كتاب النحو`
|
| 331 |
+
- **Status:** ⚠️ Unchanged
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
## 3. Punctuation Bugs
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
### 3.1. Destruction of Title/List Colons
|
| 338 |
+
|
| 339 |
+
**Example 1:**
|
| 340 |
+
- **Original:** `الخلاصة: هذا هو الموضوع`
|
| 341 |
+
- **Result:** `الخلاصة هذا هو الموضوع`
|
| 342 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 343 |
+
|
| 344 |
+
**Example 2:**
|
| 345 |
+
- **Original:** `الفصل الأول: البداية`
|
| 346 |
+
- **Result:** `الفصل الأول البداية`
|
| 347 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
### 3.2. Spelling Regressions Allowed
|
| 351 |
+
|
| 352 |
+
**Example 1:**
|
| 353 |
+
- **Original:** `Spelling regression (أحمد -> احمد،)`
|
| 354 |
+
- **Result:** `True`
|
| 355 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 356 |
+
|
| 357 |
+
**Example 2:**
|
| 358 |
+
- **Original:** `Spelling regression (مدرسة -> مدرسه.)`
|
| 359 |
+
- **Result:** `True`
|
| 360 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
### 3.3. Colon Relocation Changing Meaning
|
| 364 |
+
|
| 365 |
+
**Example 1:**
|
| 366 |
+
- **Original:** `قال: المعلم قادم`
|
| 367 |
+
- **Result:** `قال المعلم: قادم`
|
| 368 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 369 |
+
|
| 370 |
+
**Example 2:**
|
| 371 |
+
- **Original:** `صرح: الوزير مشغول`
|
| 372 |
+
- **Result:** `صرح الوزير: مشغول`
|
| 373 |
+
- **Status:** ❌ Failed (Bug Triggered)
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
## 4. Global Structural Bugs
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
### 4.1. Punctuation Masking Dictionary Lookups
|
| 380 |
+
|
| 381 |
+
**Example 1:**
|
| 382 |
+
- **Original:** `اعلن.`
|
| 383 |
+
- **Result:** `اعلن.`
|
| 384 |
+
- **Status:** ❌ Failed (Bug Triggered - Rule Bypassed)
|
| 385 |
+
|
| 386 |
+
**Example 2:**
|
| 387 |
+
- **Original:** `اصدر،`
|
| 388 |
+
- **Result:** `اصدر،`
|
| 389 |
+
- **Status:** ❌ Failed (Bug Triggered - Rule Bypassed)
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
### 4.2. Unrestrained Number Hallucination
|
| 393 |
+
|
| 394 |
+
**Example 1:**
|
| 395 |
+
- **Original:** `النص بدون أرقام`
|
| 396 |
+
- **Result:** `hallucinated 123`
|
| 397 |
+
- **Status:** ❌ Failed (Bug Triggered - Number Allowed)
|
| 398 |
+
|
| 399 |
+
**Example 2:**
|
| 400 |
+
- **Original:** `لا يوجد رقم هنا`
|
| 401 |
+
- **Result:** `hallucinated 123`
|
| 402 |
+
- **Status:** ❌ Failed (Bug Triggered - Number Allowed)
|
check_dividers.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
with open('src/index.html', 'r', encoding='utf-8') as f:
|
| 3 |
+
html = f.read()
|
| 4 |
+
matches = re.findall(r'<div class="h-[67] w-\[1\.5px\] bg-gray-300 dark:bg-gray-700 rounded-full"></div>', html)
|
| 5 |
+
print(f'Found {len(matches)} dividers.')
|
check_fp.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
with open('tests/phase10/reports/phase10_results.json', 'r', encoding='utf-8') as f:
|
| 3 |
+
d = json.load(f)
|
| 4 |
+
for r in d['results']:
|
| 5 |
+
if r['pipeline_verdict'] == 'FP' and r['dataset'] == 'hallucination':
|
| 6 |
+
print(f"{r['id']}: {r['pipeline_detail']}")
|
debug.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
with open('LOGOS/icon128.png', 'rb') as img:
|
| 5 |
+
data_uri = 'data:image/png;base64,' + base64.b64encode(img.read()).decode('utf-8')
|
| 6 |
+
|
| 7 |
+
with open('src/index.html', 'r', encoding='utf-8') as f:
|
| 8 |
+
html = f.read()
|
| 9 |
+
|
| 10 |
+
match = re.search(r'src="data:image/png;base64,([A-Za-z0-9+/=]+)"', html)
|
| 11 |
+
if match:
|
| 12 |
+
old_b64 = match.group(1)
|
| 13 |
+
print('Match found!')
|
| 14 |
+
if old_b64 == data_uri.split(',')[1]:
|
| 15 |
+
print('The base64 in index.html is EXACTLY THE SAME as LOGOS/icon128.png')
|
| 16 |
+
else:
|
| 17 |
+
print('They are DIFFERENT. Length old:', len(old_b64), 'Length new:', len(data_uri.split(',')[1]))
|
| 18 |
+
else:
|
| 19 |
+
print('No match found for the regex!')
|
extension/IMPLEMENTATION_CHANGELOG.md
DELETED
|
@@ -1,151 +0,0 @@
|
|
| 1 |
-
# BAYAN Extension — Implementation Changelog
|
| 2 |
-
|
| 3 |
-
> **Session date:** 2026-06-27
|
| 4 |
-
> **Scope:** Chrome extension only (`extension/`). No backend, website, or Supabase changes.
|
| 5 |
-
> **Source of work:** Implementation of the extension-facing items from `BAYAN_COMPLETE_AUDIT.md`.
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## ⚠️ Important context
|
| 10 |
-
|
| 11 |
-
While this work was in progress, **a second process was editing the same extension files in parallel**, implementing the same audit plan. To avoid corrupting files, this session was scoped to the **untouched gaps**. As a result, the changes below fall into two groups:
|
| 12 |
-
|
| 13 |
-
- **Authored in this session** — Phase 5 (TXT export), Phase 6 (English locale, `all_frames`, bug fixes B4/U1), and the **completion of the broken Phase 4** (inline ghost-text autocomplete).
|
| 14 |
-
- **Verified only** — Phases 1–3 (API client functions, popup/side-panel feature tabs) were written by the concurrent process; this session confirmed they are correct against the real backend (`src/app.py`) but did not author them.
|
| 15 |
-
|
| 16 |
-
This changelog documents **what was authored in this session**.
|
| 17 |
-
|
| 18 |
-
---
|
| 19 |
-
|
| 20 |
-
## Files changed
|
| 21 |
-
|
| 22 |
-
| File | Type | What changed |
|
| 23 |
-
|------|------|--------------|
|
| 24 |
-
| `extension/content-inline.js` | **Edited** | Implemented the missing inline ghost-text autocomplete engine (Phase 4); overlay reposition on resize (B4); tooltip viewport clamping (U1) |
|
| 25 |
-
| `extension/content-inline.css` | **Edited** | Styles for the ghost-text mirror + muted suffix |
|
| 26 |
-
| `extension/popup.js` | **Edited** | "Download as TXT" buttons for corrected text + summary (Phase 5 / H3) |
|
| 27 |
-
| `extension/sidepanel/sidepanel.js` | **Edited** | "Download as TXT" buttons for corrected text + summary (Phase 5 / H3) |
|
| 28 |
-
| `extension/manifest.json` | **Edited** | `all_frames: true` for iframe editor support (M2) |
|
| 29 |
-
| `extension/_locales/en/messages.json` | **Created** | English locale (L1) |
|
| 30 |
-
|
| 31 |
-
> `extension/shared/bayan-api.js`, `extension/popup.html`, `extension/sidepanel/sidepanel.html`, and the dialect/quran/autocomplete handlers in `popup.js` / `sidepanel.js` were authored by the **concurrent process**, not this session. They are listed in the "Verified only" section at the end.
|
| 32 |
-
|
| 33 |
-
---
|
| 34 |
-
|
| 35 |
-
## 1. Phase 4 — Inline ghost-text autocomplete (audit item H2)
|
| 36 |
-
|
| 37 |
-
**File:** `extension/content-inline.js`, `extension/content-inline.css`
|
| 38 |
-
|
| 39 |
-
### The bug this fixed (regression)
|
| 40 |
-
|
| 41 |
-
The concurrent process had added ghost-text **state variables** and a **call** to `scheduleGhost()` on every keystroke (in `onFieldInput`), but **never defined `scheduleGhost` or any ghost logic**. The content script therefore threw:
|
| 42 |
-
|
| 43 |
-
```
|
| 44 |
-
ReferenceError: scheduleGhost is not defined
|
| 45 |
-
```
|
| 46 |
-
|
| 47 |
-
…on **every keystroke in any editable field**, which silently broke inline analysis on every website. This was an active regression, not just a missing feature.
|
| 48 |
-
|
| 49 |
-
### What was implemented
|
| 50 |
-
|
| 51 |
-
A Tab-to-accept ghost-text engine for 3rd-party `<textarea>` / `<input>` fields, ported from the website's `src/js/autocomplete.js` behavior:
|
| 52 |
-
|
| 53 |
-
| Function | Role |
|
| 54 |
-
|----------|------|
|
| 55 |
-
| `ghostEligible()` | Only fires when the caret is collapsed **at the end** of the field, ≥3 chars, Arabic present |
|
| 56 |
-
| `scheduleGhost()` | 450 ms debounce; clears any stale ghost first |
|
| 57 |
-
| `fetchGhost(ctx)` | `POST {BAYAN.API_BASE}/api/autocomplete` with `{ context, n: 1 }`; staleness-guarded |
|
| 58 |
-
| `showGhost(base, suffix)` | Transparent-mirror overlay (same technique as the error overlay) painting the completion in muted grey at the caret |
|
| 59 |
-
| `acceptGhost()` | **Tab** appends the suggestion (+leading space if needed), moves caret to end, dispatches `input` to re-trigger analysis + next-word ghost |
|
| 60 |
-
| `clearGhost()` | Teardown — cancels timer, removes overlay, resets state |
|
| 61 |
-
| `syncGhostScroll()` | Keeps the ghost aligned with field scroll |
|
| 62 |
-
| `onFieldKeydown(e)` | **Tab** = accept, **Escape** = dismiss |
|
| 63 |
-
|
| 64 |
-
### Wiring added
|
| 65 |
-
- `keydown` → `onFieldKeydown` registered in `attachField()`, removed in `detachField()`.
|
| 66 |
-
- `clearGhost()` added to `detachField()` teardown.
|
| 67 |
-
- `syncOverlay()` now also calls `syncGhostScroll()`.
|
| 68 |
-
- The window `scroll` handler now repositions the ghost overlay (alongside the error overlay).
|
| 69 |
-
|
| 70 |
-
### Why a direct `fetch` (not `bayanAutocomplete`)
|
| 71 |
-
The content script only loads `shared/constants.js`, `shared/analysis-controller.js`, and `content-inline.js` — **not** `bayan-api.js`/`config.js`. So `bayanAutocomplete()`/`CONFIG` are out of scope. `BAYAN.API_BASE` (from `constants.js`) **is** in scope, and `host_permissions` already covers that host, so the call is made directly.
|
| 72 |
-
|
| 73 |
-
### Safety properties
|
| 74 |
-
- Overlay-only: never mutates the field while typing — only on explicit **Tab**.
|
| 75 |
-
- Best-effort: network/render errors are swallowed; the analysis path is never affected.
|
| 76 |
-
- Respects protected sites and is gated to `textarea`/`input` only.
|
| 77 |
-
|
| 78 |
-
### CSS added (`content-inline.css`)
|
| 79 |
-
```css
|
| 80 |
-
.bayan-il-ghost { background: transparent !important; pointer-events: none !important; }
|
| 81 |
-
.bayan-il-ghost-suffix { color: rgba(120,120,130,0.75) !important; opacity: 0.9; }
|
| 82 |
-
```
|
| 83 |
-
(The suffix needs an explicit color because the mirror parent is `color: transparent`.)
|
| 84 |
-
|
| 85 |
-
---
|
| 86 |
-
|
| 87 |
-
## 2. Phase 5 — Download corrected text / summary as TXT (audit item H3)
|
| 88 |
-
|
| 89 |
-
**Files:** `extension/popup.js`, `extension/sidepanel/sidepanel.js`
|
| 90 |
-
|
| 91 |
-
- Added a self-contained `downloadTxt(text, filename)` helper (Blob + object URL + cleanup).
|
| 92 |
-
- Download buttons are **injected programmatically** next to the existing copy buttons — no HTML edits, to minimize conflict with the concurrent writer.
|
| 93 |
-
- Popup: downloads `bayan-corrected.txt` and `bayan-summary.txt`.
|
| 94 |
-
- Side panel: same, anchored to the existing copy buttons' parent containers.
|
| 95 |
-
|
| 96 |
-
---
|
| 97 |
-
|
| 98 |
-
## 3. Phase 6 — Polish & bug fixes
|
| 99 |
-
|
| 100 |
-
### B4 — Overlay reposition on resize (`content-inline.js`)
|
| 101 |
-
The window `resize` handler previously repositioned only the floating button. It now also **re-renders the error overlay** when suggestions are present, so highlights stay aligned after a viewport resize.
|
| 102 |
-
|
| 103 |
-
### U1 — Tooltip viewport clamping (`content-inline.js`)
|
| 104 |
-
The error tooltip previously clamped only the right edge. It now also:
|
| 105 |
-
- clamps the **left** edge if it overflows,
|
| 106 |
-
- **flips above** the highlighted mark when it would overflow the **bottom** of the viewport (falling back to pinning at the top edge if there's no room above).
|
| 107 |
-
|
| 108 |
-
### M2 — iframe editor support (`manifest.json`)
|
| 109 |
-
Added `"all_frames": true` to the content-script registration so editors inside `<iframe>` (TinyMCE, Gutenberg, etc.) are reachable.
|
| 110 |
-
|
| 111 |
-
### L1 — English locale (`_locales/en/messages.json`, new file)
|
| 112 |
-
Created an English `messages.json` mirroring the Arabic keys (`extName`, `extDescription`, `contextMenuCorrect`, `contextMenuSummarize`) so the extension is publishable for non-Arabic users.
|
| 113 |
-
|
| 114 |
-
---
|
| 115 |
-
|
| 116 |
-
## Verification performed
|
| 117 |
-
|
| 118 |
-
- `node --check` passed on all edited JS files: `content-inline.js`, `popup.js`, `sidepanel/sidepanel.js`, `shared/bayan-api.js`.
|
| 119 |
-
- JSON validated: `manifest.json`, `_locales/en/messages.json`, `_locales/ar/messages.json`.
|
| 120 |
-
- Confirmed all eight ghost-text functions are defined, so the previously-orphaned `scheduleGhost()` call now resolves.
|
| 121 |
-
|
| 122 |
-
### NOT verified (no runtime/browser test in this session)
|
| 123 |
-
- The extension was **not** loaded in Chrome. Caret-position rendering of ghost text across real sites, the new tabs, downloads, and iframe behavior are unverified by execution. See the testing guide below.
|
| 124 |
-
|
| 125 |
-
---
|
| 126 |
-
|
| 127 |
-
## How to test (manual, in Chrome)
|
| 128 |
-
|
| 129 |
-
The backend (`bayan10-bayan-api.hf.space`) is already live, so no local server is needed.
|
| 130 |
-
|
| 131 |
-
1. **Load:** `chrome://extensions` → enable **Developer mode** → **Load unpacked** → select `extension/`. Confirm no errors on the card; open the **service worker** console and confirm it's clean.
|
| 132 |
-
2. **Popup tabs:** open the popup → expect 5 tabs (تصحيح · تلخيص · لهجة · قرآن · إكمال). Exercise each.
|
| 133 |
-
3. **Export:** analyze text → click the download (↓) icon in the result header → `bayan-corrected.txt` downloads.
|
| 134 |
-
4. **Ghost text (H2):** in a plain `<textarea>` (e.g. `extension/tests/test_inline.html`), type a few Arabic words ending in a space → grey ghost text appears after ~0.5 s → **Tab** inserts, **Esc** dismisses. Confirm **no** `scheduleGhost is not defined` error in the page console.
|
| 135 |
-
5. **Regression checks:** resize the window with errors highlighted (B4 — stays aligned); click an error near the bottom (U1 — tooltip flips up); try a textarea inside an iframe (M2).
|
| 136 |
-
|
| 137 |
-
---
|
| 138 |
-
|
| 139 |
-
## Authored by the concurrent process (verified, not authored here)
|
| 140 |
-
|
| 141 |
-
For completeness — these extension changes exist but were **not** written in this session:
|
| 142 |
-
|
| 143 |
-
- `extension/shared/bayan-api.js` — `bayanDialect()`, `bayanQuran()`, `bayanAutocomplete()` (audit B3).
|
| 144 |
-
- `extension/popup.html` + `extension/popup.js` — dialect / quran / autocomplete tabs + handlers (U3).
|
| 145 |
-
- `extension/sidepanel/sidepanel.html` + `extension/sidepanel/sidepanel.js` — same three feature tabs + handlers.
|
| 146 |
-
|
| 147 |
-
---
|
| 148 |
-
|
| 149 |
-
## Out of scope (NOT done — still open from the 52-item audit)
|
| 150 |
-
|
| 151 |
-
This session covered extension items only. The following remain **open**: all Critical/Security backend items (C1–C3, S1–S7), extension auth (H1), the `002_documents.sql` migration (H4), the `app.py` split (H5), and the bulk of the Medium/Low backend, website, and performance items. See `BAYAN_COMPLETE_AUDIT.md` for the full list.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/PLAN_apply_contextmenu_reanalysis.md
DELETED
|
@@ -1,262 +0,0 @@
|
|
| 1 |
-
# BAYAN Extension — Implementation Plan: Apply-to-Field, Context Menu Features & Smart Re-Analysis
|
| 2 |
-
|
| 3 |
-
> **For:** the coding agent implementing these changes.
|
| 4 |
-
> **Scope:** Chrome extension only (`extension/`). No backend changes.
|
| 5 |
-
> **Read this whole file before editing.** Each change lists the exact files, the current behavior, the target behavior, and the wiring required. Implement the three changes in order — Change 1 builds the messaging channel that Change 3 reuses.
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## Background: how the pieces currently connect
|
| 10 |
-
|
| 11 |
-
- **`content-inline.js`** — injected into every page. Detects editable fields (`textarea`, `input`, `contenteditable`), analyzes them, and renders an overlay + tooltip. It **already writes corrections back** to the page field via `applyFix()`. It currently has **no `chrome.runtime.onMessage` listener** (it only *sends* messages).
|
| 12 |
-
- **`background.js`** — service worker. Owns the context menu and the API bridge. Registers exactly two menu items: `bayan-correct`, `bayan-summarize`. On click it opens the side panel and stashes `{contextAction, contextText}` in `chrome.storage.session`.
|
| 13 |
-
- **`sidepanel/sidepanel.js`** — reads that stashed context on open, switches to the matching tab, and auto-runs. It has `correct`, `summarize`, `dialect`, `quran`, `autocomplete` tabs and handlers. Its apply / apply-all buttons currently **only edit the side panel's own textarea** — they do **not** touch the page field.
|
| 14 |
-
- **`popup.js`** — same apply logic as the side panel, also only editing its own textarea.
|
| 15 |
-
|
| 16 |
-
Key gap for Changes 1 & 3: **the panel surfaces have no link back to the page field the text came from.** We must establish that link.
|
| 17 |
-
|
| 18 |
-
---
|
| 19 |
-
|
| 20 |
-
## CHANGE 1 — "Apply" / "Apply all" writes the result into the user's actual text field
|
| 21 |
-
|
| 22 |
-
### Current behavior
|
| 23 |
-
- **Inline tooltip apply** (`content-inline.js` → `applyFix`): ✅ already writes back to the page field and dispatches an `input` event. No change needed here.
|
| 24 |
-
- **Side panel & popup apply / apply-all**: ❌ only mutate the panel's own `<textarea id="input-text">`. The user's text field on the page is untouched.
|
| 25 |
-
|
| 26 |
-
### Target behavior
|
| 27 |
-
When the user clicks **Apply** (single suggestion) or **Apply all** in the **side panel** (and popup where applicable), the corrected text is written into the **original page field** that the text came from — replacing the selection, or the whole field if appropriate — and an `input` event is dispatched so the host page registers the change.
|
| 28 |
-
|
| 29 |
-
### Why this needs a messaging channel
|
| 30 |
-
The side panel is a separate document; it cannot touch the page DOM directly. It must send a message → background → content script → content script writes into the field.
|
| 31 |
-
|
| 32 |
-
### Implementation
|
| 33 |
-
|
| 34 |
-
**1.1 — `content-inline.js`: track the "source field" and expose a write-back handler**
|
| 35 |
-
|
| 36 |
-
- Add module state: `let lastInteractedField = null;`
|
| 37 |
-
- In `attachField(field)` (or `focusin`), set `lastInteractedField = field;` whenever a real editable field is focused. (Keep `activeField` semantics as-is; `lastInteractedField` persists even after focus moves to the side panel, which `activeField`/`detachField` would clear.)
|
| 38 |
-
- When the FAB sends `OPEN_SIDEPANEL` (existing code ~line 548), also include a stable identifier so we can re-find the field. Simplest robust approach: **tag the field** with a data attribute when opening the panel:
|
| 39 |
-
```js
|
| 40 |
-
// before sending OPEN_SIDEPANEL
|
| 41 |
-
if (lastInteractedField) lastInteractedField.dataset.bayanSource = '1';
|
| 42 |
-
```
|
| 43 |
-
- Add a **`chrome.runtime.onMessage` listener** (the content script currently has none):
|
| 44 |
-
```js
|
| 45 |
-
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
| 46 |
-
if (msg.type === 'BAYAN_WRITE_BACK') {
|
| 47 |
-
const field = lastInteractedField
|
| 48 |
-
|| document.querySelector('[data-bayan-source="1"]')
|
| 49 |
-
|| (isEditableField(document.activeElement) ? document.activeElement : null);
|
| 50 |
-
if (!field) { sendResponse({ ok: false, reason: 'no_field' }); return true; }
|
| 51 |
-
writeTextToField(field, msg.text, msg.mode); // mode: 'replaceAll' | 'replaceSelection'
|
| 52 |
-
sendResponse({ ok: true });
|
| 53 |
-
return true;
|
| 54 |
-
}
|
| 55 |
-
return false;
|
| 56 |
-
});
|
| 57 |
-
```
|
| 58 |
-
- Implement `writeTextToField(field, text, mode)`:
|
| 59 |
-
- For `textarea`/`input`: if `mode === 'replaceSelection'` and `selectionStart !== selectionEnd`, splice into `field.value` at the selection; else set `field.value = text`. Then `field.setSelectionRange(end, end)` and dispatch `new Event('input', { bubbles: true })`.
|
| 60 |
-
- For `contenteditable`: focus the field and use `document.execCommand('insertText', false, text)` when there's a live selection inside it; otherwise set `field.textContent = text` and dispatch `input`. (Mirror the overlay-safe approach already used in `applyFix`.)
|
| 61 |
-
- **Set the suppression flag from Change 3** right before dispatching `input` (see Change 3) when the write came from a non-correction model. For Change 1's correction apply-back, do **not** suppress — corrected text re-analyzing is harmless/expected.
|
| 62 |
-
- Clear `data-bayan-source` after writing.
|
| 63 |
-
|
| 64 |
-
**1.2 — `background.js`: relay panel → content script**
|
| 65 |
-
|
| 66 |
-
- Add a message handler branch:
|
| 67 |
-
```js
|
| 68 |
-
if (message.type === 'WRITE_BACK_TO_PAGE') {
|
| 69 |
-
// forward to the active tab's content script
|
| 70 |
-
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
| 71 |
-
const tab = tabs[0];
|
| 72 |
-
if (!tab) { sendResponse({ ok: false }); return; }
|
| 73 |
-
chrome.tabs.sendMessage(tab.id, {
|
| 74 |
-
type: 'BAYAN_WRITE_BACK', text: message.text, mode: message.mode || 'replaceAll', source: message.source
|
| 75 |
-
}, (resp) => sendResponse(resp || { ok: false }));
|
| 76 |
-
});
|
| 77 |
-
return true; // async
|
| 78 |
-
}
|
| 79 |
-
```
|
| 80 |
-
|
| 81 |
-
**1.3 — `sidepanel/sidepanel.js`: send write-back after apply / apply-all**
|
| 82 |
-
|
| 83 |
-
- Add a helper:
|
| 84 |
-
```js
|
| 85 |
-
function writeBackToPage(text, mode = 'replaceAll', source = 'correct') {
|
| 86 |
-
chrome.runtime.sendMessage(
|
| 87 |
-
{ type: 'WRITE_BACK_TO_PAGE', text, mode, source },
|
| 88 |
-
(resp) => {
|
| 89 |
-
if (resp && resp.ok) showToast('✓ تم تطبيق التغييرات في الصفحة');
|
| 90 |
-
else showToast('تعذّر الكتابة في الصفحة — انسخ النص يدوياً');
|
| 91 |
-
}
|
| 92 |
-
);
|
| 93 |
-
}
|
| 94 |
-
```
|
| 95 |
-
- In the existing **apply-all** handler (after `analyzedText = applyAllPatches(...)`), call `writeBackToPage(analyzedText, 'replaceAll', 'correct');`.
|
| 96 |
-
- In the single-suggestion **apply** path (after `applyAndRebase` updates `analyzedText`), call `writeBackToPage(analyzedText, 'replaceAll', 'correct');`. (Whole-field replace is simplest and avoids offset drift between the panel copy and the live field.)
|
| 97 |
-
|
| 98 |
-
**1.4 — `popup.js`** (optional, lower priority)
|
| 99 |
-
- The popup closes when it loses focus, so write-back is less reliable there. Apply the same `writeBackToPage` helper **only if** product wants it; otherwise leave the popup apply editing its own textarea and rely on copy/download. Document the decision in a comment.
|
| 100 |
-
|
| 101 |
-
### Acceptance criteria
|
| 102 |
-
- Select/focus a page `<textarea>`, open the side panel via the FAB, click **Apply all** → the page textarea content is replaced with the corrected text and the host page sees an `input` event.
|
| 103 |
-
- Works for a single **Apply** too.
|
| 104 |
-
- If the source field can't be found, the user gets a clear toast (no silent failure).
|
| 105 |
-
|
| 106 |
-
---
|
| 107 |
-
|
| 108 |
-
## CHANGE 2 — Add "لهجات" and "قرآن" to the right-click context menu
|
| 109 |
-
|
| 110 |
-
### Current behavior
|
| 111 |
-
`background.js` registers only:
|
| 112 |
-
- `bayan-correct` → "تصحيح مع بيان"
|
| 113 |
-
- `bayan-summarize` → "تلخيص مع بيان"
|
| 114 |
-
|
| 115 |
-
### Target behavior
|
| 116 |
-
The selection context menu shows **four** Bayan items:
|
| 117 |
-
- تصحيح مع بيان (existing)
|
| 118 |
-
- تلخيص مع بيان (existing)
|
| 119 |
-
- **تحويل اللهجة إلى الفصحى مع بيان** (new)
|
| 120 |
-
- **تدقيق الآية مع بيان** (new)
|
| 121 |
-
|
| 122 |
-
Clicking a new item opens the side panel, switches to the matching tab, fills the selected text, and auto-runs that model.
|
| 123 |
-
|
| 124 |
-
### Implementation
|
| 125 |
-
|
| 126 |
-
**2.1 — `background.js`**
|
| 127 |
-
- Extend the actions map:
|
| 128 |
-
```js
|
| 129 |
-
const ACTIONS = { CORRECT: 'correct', SUMMARIZE: 'summarize', DIALECT: 'dialect', QURAN: 'quran' };
|
| 130 |
-
```
|
| 131 |
-
- In `chrome.runtime.onInstalled` add two `chrome.contextMenus.create(...)` calls:
|
| 132 |
-
```js
|
| 133 |
-
chrome.contextMenus.create({ id: 'bayan-dialect',
|
| 134 |
-
title: chrome.i18n.getMessage('contextMenuDialect') || 'تحويل اللهجة إلى الفصحى مع بيان',
|
| 135 |
-
contexts: ['selection'] });
|
| 136 |
-
chrome.contextMenus.create({ id: 'bayan-quran',
|
| 137 |
-
title: chrome.i18n.getMessage('contextMenuQuran') || 'تدقيق الآية مع بيان',
|
| 138 |
-
contexts: ['selection'] });
|
| 139 |
-
```
|
| 140 |
-
- In `chrome.contextMenus.onClicked`, add routing:
|
| 141 |
-
```js
|
| 142 |
-
if (info.menuItemId === 'bayan-dialect') action = ACTIONS.DIALECT;
|
| 143 |
-
if (info.menuItemId === 'bayan-quran') action = ACTIONS.QURAN;
|
| 144 |
-
```
|
| 145 |
-
The rest of the handler (open side panel + stash context) already works generically.
|
| 146 |
-
|
| 147 |
-
**2.2 — `sidepanel/sidepanel.js`** — handle the two new context actions on pickup
|
| 148 |
-
- The side panel already reads `contextAction` and, for `correct`/`summarize`, switches tab + auto-runs. Extend **both** pickup paths (`tryPickupContext` AND the `storage.onChanged` listener) to handle the new actions:
|
| 149 |
-
```js
|
| 150 |
-
} else if (action === 'dialect') {
|
| 151 |
-
dialectInput.value = text; updateCounts(dialectInput, dialectCharCount, null);
|
| 152 |
-
document.querySelector('[data-tab="dialect"]')?.click();
|
| 153 |
-
setTimeout(() => btnDialect.click(), 120);
|
| 154 |
-
} else if (action === 'quran') {
|
| 155 |
-
quranInput.value = text; updateCounts(quranInput, quranCharCount, null);
|
| 156 |
-
document.querySelector('[data-tab="quran"]')?.click();
|
| 157 |
-
setTimeout(() => btnQuran.click(), 120);
|
| 158 |
-
}
|
| 159 |
-
```
|
| 160 |
-
(Hoist `dialectInput`, `btnDialect`, `quranInput`, `btnQuran`, etc. so they're in scope of the pickup functions, or wrap the pickup dispatch in a small `runContextAction(action, text)` function declared after all element refs.)
|
| 161 |
-
- Update the `TAB` constant if it's used for validation: `const TAB = { CORRECT:'correct', SUMMARIZE:'summarize', DIALECT:'dialect', QURAN:'quran' };`
|
| 162 |
-
|
| 163 |
-
**2.3 — Locales** — add the two new menu strings
|
| 164 |
-
- `_locales/ar/messages.json`:
|
| 165 |
-
```json
|
| 166 |
-
"contextMenuDialect": { "message": "تحويل اللهجة إلى الفصحى مع بيان", "description": "Context menu: dialect→MSA" },
|
| 167 |
-
"contextMenuQuran": { "message": "تدقيق الآية مع بيان", "description": "Context menu: Quran verify" }
|
| 168 |
-
```
|
| 169 |
-
- `_locales/en/messages.json`:
|
| 170 |
-
```json
|
| 171 |
-
"contextMenuDialect": { "message": "Convert dialect to MSA with Bayan", "description": "Context menu: dialect→MSA" },
|
| 172 |
-
"contextMenuQuran": { "message": "Verify verse with Bayan", "description": "Context menu: Quran verify" }
|
| 173 |
-
```
|
| 174 |
-
|
| 175 |
-
### Acceptance criteria
|
| 176 |
-
- Right-clicking selected Arabic text shows all four Bayan items.
|
| 177 |
-
- "تحويل اللهجة…" opens the side panel on the **لهجة** tab with the text filled and converted.
|
| 178 |
-
- "تدقيق الآية…" opens the side panel on the **قرآن** tab with the text filled and checked.
|
| 179 |
-
- Existing correct/summarize items are unchanged.
|
| 180 |
-
|
| 181 |
-
---
|
| 182 |
-
|
| 183 |
-
## CHANGE 3 — After applying summarize / dialect / quran output into a field, do NOT auto-run the correction model on it
|
| 184 |
-
|
| 185 |
-
### The problem
|
| 186 |
-
`content-inline.js` analyzes editable fields on every keystroke and on programmatic `input` events. If text written back into the field came from the **summarize**, **dialect**, or **quran** model (Change 1's write-back for those flows), the correction pipeline would immediately re-analyze it — which the user does **not** want (a summary/MSA/verse is the intended final text, not something to "correct").
|
| 187 |
-
|
| 188 |
-
> Note: for the **correction** apply-back (Change 1), re-analysis is fine/expected and must stay enabled.
|
| 189 |
-
|
| 190 |
-
### Target behavior
|
| 191 |
-
When a write-back originates from a **non-correction** model (`summarize` / `dialect` / `quran`), the content script must **suppress correction analysis** for that field until the **user makes a genuine manual edit** (a real keystroke), at which point normal analysis resumes.
|
| 192 |
-
|
| 193 |
-
### Implementation (`content-inline.js`)
|
| 194 |
-
|
| 195 |
-
- Add module state:
|
| 196 |
-
```js
|
| 197 |
-
let analysisSuppressed = false; // true after non-correction model write-back
|
| 198 |
-
```
|
| 199 |
-
- Extend `writeTextToField(field, text, mode)` (from Change 1) to accept the `source`/`mode` info, and:
|
| 200 |
-
- If the write-back `source` is one of `summarize|dialect|quran`, set `analysisSuppressed = true;` **before** dispatching the `input` event.
|
| 201 |
-
- If the `source` is `correct` (or inline correction apply), leave `analysisSuppressed = false`.
|
| 202 |
-
- In `onFieldInput()` (the input handler), **gate the analysis** but distinguish programmatic vs. human input. The cleanest signal: the synthetic `input` event we dispatch is not trusted. So:
|
| 203 |
-
```js
|
| 204 |
-
function onFieldInput(e) {
|
| 205 |
-
if (paused || !activeField) return;
|
| 206 |
-
|
| 207 |
-
const programmatic = e && e.isTrusted === false;
|
| 208 |
-
// A genuine user keystroke clears suppression and re-enables analysis.
|
| 209 |
-
if (!programmatic && analysisSuppressed) analysisSuppressed = false;
|
| 210 |
-
|
| 211 |
-
// Ghost-text autocomplete still runs (it's not the correction model).
|
| 212 |
-
scheduleGhost();
|
| 213 |
-
|
| 214 |
-
if (analysisSuppressed) { // model output just written — skip correction
|
| 215 |
-
clearHighlights();
|
| 216 |
-
updateBadge(0);
|
| 217 |
-
return;
|
| 218 |
-
}
|
| 219 |
-
// ...existing analysis path unchanged...
|
| 220 |
-
}
|
| 221 |
-
```
|
| 222 |
-
- Ensure `onFieldInput` is registered so it receives the event object (it's added via `field.addEventListener('input', onFieldInput)` — the handler already receives `e`).
|
| 223 |
-
- The write-back's dispatched event uses `new Event('input', {bubbles:true})`, whose `isTrusted` is `false` — this is the reliable discriminator between "we wrote this" and "the user typed."
|
| 224 |
-
- Edge cases to handle:
|
| 225 |
-
- **Badge state:** when suppressed, show the clean/✓ badge (0), not the analyzing spinner.
|
| 226 |
-
- **Decide on ghost-text:** keep autocomplete ghost active (it's a separate, opt-in feature) OR also suppress it for summarize/quran outputs if product prefers a fully "frozen" result. Default: keep ghost on; add a one-line comment noting the choice.
|
| 227 |
-
- **Re-enable correctness:** the very next real keystroke must restore analysis. Verify by typing one character after a dialect write-back → underlines should come back.
|
| 228 |
-
|
| 229 |
-
### Acceptance criteria
|
| 230 |
-
- Apply a **dialect** (or summarize/quran) result into a page field via Change 1 → the field shows the model output with **no correction underlines** and the FAB badge is not in an error/analyzing state.
|
| 231 |
-
- Type one character in that field → correction analysis resumes normally.
|
| 232 |
-
- Applying a **correction** result still re-analyzes as before (suppression must NOT trigger for `source: 'correct'`).
|
| 233 |
-
|
| 234 |
-
---
|
| 235 |
-
|
| 236 |
-
## Suggested implementation order & testing
|
| 237 |
-
|
| 238 |
-
1. **Change 2** first (self-contained, no messaging) — fastest win, easy to verify.
|
| 239 |
-
2. **Change 1** (build the messaging channel: content-script `onMessage` + `writeTextToField` + background relay + side-panel `writeBackToPage`).
|
| 240 |
-
3. **Change 3** (reuses Change 1's `writeTextToField` + `source` flag).
|
| 241 |
-
|
| 242 |
-
### Manual test pass (Chrome, `chrome://extensions` → Load unpacked → `extension/`)
|
| 243 |
-
- Reload the extension after editing the service worker / manifest (context menus only re-register on install/update — use the reload button).
|
| 244 |
-
- **Change 2:** right-click selected Arabic text → confirm 4 items → click لهجات and قرآن → correct tab opens + auto-runs.
|
| 245 |
-
- **Change 1:** focus a page `<textarea>`, open side panel via FAB, Apply / Apply all → page field updates + host page sees the change.
|
| 246 |
-
- **Change 3:** send a dialect result back to the field → no correction underlines → type one char → underlines return. Confirm a correction apply-back still re-analyzes.
|
| 247 |
-
|
| 248 |
-
### Files touched (summary)
|
| 249 |
-
| File | Change 1 | Change 2 | Change 3 |
|
| 250 |
-
|------|:---:|:---:|:---:|
|
| 251 |
-
| `extension/content-inline.js` | ✅ onMessage + `writeTextToField` + `lastInteractedField` | — | ✅ `analysisSuppressed` gate |
|
| 252 |
-
| `extension/background.js` | ✅ `WRITE_BACK_TO_PAGE` relay | ✅ 2 menu items + routing | — |
|
| 253 |
-
| `extension/sidepanel/sidepanel.js` | ✅ `writeBackToPage` on apply/apply-all | ✅ pickup for dialect/quran | ✅ pass correct `source` |
|
| 254 |
-
| `extension/popup.js` | ⚠️ optional | — | — |
|
| 255 |
-
| `extension/_locales/ar/messages.json` | — | ✅ 2 strings | — |
|
| 256 |
-
| `extension/_locales/en/messages.json` | — | ✅ 2 strings | — |
|
| 257 |
-
|
| 258 |
-
### Guardrails
|
| 259 |
-
- Don't break the existing inline `applyFix` write-back — it already works; reuse its patterns, don't replace it.
|
| 260 |
-
- All cross-document communication goes panel → `background.js` → content script. The side panel must never assume direct page DOM access.
|
| 261 |
-
- Use `event.isTrusted === false` (not a custom flag on the event) to detect programmatic input — it's tamper-proof and requires no host-page cooperation.
|
| 262 |
-
- Keep every new code path guarded (`if (!field) ...`, `resp || {ok:false}`) so a missing field or closed tab degrades to a toast, never an uncaught error.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/assets/icons/fab_logo.png
ADDED
|
|
Git LFS Details
|
extension/content-inline.css
CHANGED
|
@@ -7,25 +7,25 @@
|
|
| 7 |
/* ── Error highlights (contenteditable + overlay) ── */
|
| 8 |
|
| 9 |
.bayan-il-spelling {
|
| 10 |
-
background: rgba(
|
| 11 |
-
border-bottom: 2px solid #
|
| 12 |
border-radius: 2px !important;
|
| 13 |
cursor: pointer !important;
|
| 14 |
transition: background 150ms ease !important;
|
| 15 |
}
|
| 16 |
.bayan-il-spelling:hover {
|
| 17 |
-
background: rgba(
|
| 18 |
}
|
| 19 |
|
| 20 |
.bayan-il-grammar {
|
| 21 |
-
background: rgba(
|
| 22 |
-
border-bottom: 2px solid #
|
| 23 |
border-radius: 2px !important;
|
| 24 |
cursor: pointer !important;
|
| 25 |
transition: background 150ms ease !important;
|
| 26 |
}
|
| 27 |
.bayan-il-grammar:hover {
|
| 28 |
-
background: rgba(
|
| 29 |
}
|
| 30 |
|
| 31 |
.bayan-il-punctuation {
|
|
@@ -49,7 +49,7 @@
|
|
| 49 |
display: flex;
|
| 50 |
align-items: center;
|
| 51 |
justify-content: center;
|
| 52 |
-
background: #
|
| 53 |
border: 1px solid #3a3a4d !important;
|
| 54 |
border-radius: 50% !important;
|
| 55 |
cursor: pointer !important;
|
|
@@ -68,8 +68,8 @@
|
|
| 68 |
}
|
| 69 |
|
| 70 |
.bayan-il-fab:hover {
|
| 71 |
-
border-color: #
|
| 72 |
-
box-shadow: 0 2px 14px rgba(
|
| 73 |
}
|
| 74 |
|
| 75 |
/* ── Badge (on FAB) ── */
|
|
@@ -89,27 +89,27 @@
|
|
| 89 |
border-radius: 8px !important;
|
| 90 |
padding: 0 4px !important;
|
| 91 |
line-height: 1 !important;
|
| 92 |
-
border: 1.5px solid #
|
| 93 |
color: white !important;
|
| 94 |
-
background: #
|
| 95 |
}
|
| 96 |
|
| 97 |
.bayan-il-badge--clean {
|
| 98 |
-
background: #
|
| 99 |
}
|
| 100 |
|
| 101 |
.bayan-il-badge--errors {
|
| 102 |
-
background: #
|
| 103 |
animation: bayan-il-pulse 600ms ease-out !important;
|
| 104 |
}
|
| 105 |
|
| 106 |
.bayan-il-badge--analyzing {
|
| 107 |
-
background: #
|
| 108 |
animation: bayan-il-spin 1s linear infinite !important;
|
| 109 |
}
|
| 110 |
|
| 111 |
.bayan-il-badge--paused {
|
| 112 |
-
background: #
|
| 113 |
opacity: 0.7 !important;
|
| 114 |
}
|
| 115 |
|
|
@@ -132,14 +132,14 @@
|
|
| 132 |
z-index: 2147483647 !important;
|
| 133 |
min-width: 200px;
|
| 134 |
max-width: 320px;
|
| 135 |
-
background: #
|
| 136 |
border: 1px solid #3a3a4d !important;
|
| 137 |
border-radius: 10px !important;
|
| 138 |
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(
|
| 139 |
overflow: hidden;
|
| 140 |
-
font-family: 'Segoe UI', Tahoma, Arial, sans-serif !important;
|
| 141 |
font-size: 13px !important;
|
| 142 |
-
color: #
|
| 143 |
opacity: 0;
|
| 144 |
transform: translateY(4px);
|
| 145 |
transition: opacity 150ms ease, transform 150ms ease !important;
|
|
@@ -155,7 +155,7 @@
|
|
| 155 |
align-items: center !important;
|
| 156 |
justify-content: space-between !important;
|
| 157 |
padding: 8px 12px !important;
|
| 158 |
-
background: #
|
| 159 |
border-bottom: 1px solid #2d2d3d !important;
|
| 160 |
}
|
| 161 |
|
|
@@ -168,12 +168,12 @@
|
|
| 168 |
}
|
| 169 |
|
| 170 |
.bayan-il-badge-spelling {
|
| 171 |
-
background: rgba(
|
| 172 |
-
color: #
|
| 173 |
}
|
| 174 |
.bayan-il-badge-grammar {
|
| 175 |
-
background: rgba(
|
| 176 |
-
color: #
|
| 177 |
}
|
| 178 |
.bayan-il-badge-punctuation {
|
| 179 |
background: rgba(107, 201, 138, 0.15) !important;
|
|
@@ -183,14 +183,14 @@
|
|
| 183 |
.bayan-il-tooltip-close {
|
| 184 |
background: none !important;
|
| 185 |
border: none !important;
|
| 186 |
-
color: #
|
| 187 |
cursor: pointer !important;
|
| 188 |
font-size: 14px !important;
|
| 189 |
padding: 2px !important;
|
| 190 |
line-height: 1 !important;
|
| 191 |
}
|
| 192 |
.bayan-il-tooltip-close:hover {
|
| 193 |
-
color: #
|
| 194 |
}
|
| 195 |
|
| 196 |
.bayan-il-tooltip-body {
|
|
@@ -203,18 +203,18 @@
|
|
| 203 |
}
|
| 204 |
|
| 205 |
.bayan-il-tooltip-original {
|
| 206 |
-
color: #
|
| 207 |
text-decoration: line-through !important;
|
| 208 |
-
text-decoration-color: rgba(
|
| 209 |
}
|
| 210 |
|
| 211 |
.bayan-il-tooltip-arrow {
|
| 212 |
-
color: #
|
| 213 |
font-size: 12px !important;
|
| 214 |
}
|
| 215 |
|
| 216 |
.bayan-il-tooltip-correction {
|
| 217 |
-
color: #
|
| 218 |
font-weight: 600 !important;
|
| 219 |
}
|
| 220 |
|
|
@@ -223,7 +223,7 @@
|
|
| 223 |
gap: 6px !important;
|
| 224 |
padding: 8px 12px !important;
|
| 225 |
border-top: 1px solid #2d2d3d !important;
|
| 226 |
-
background: #
|
| 227 |
}
|
| 228 |
|
| 229 |
.bayan-il-tooltip-apply {
|
|
@@ -231,9 +231,9 @@
|
|
| 231 |
padding: 5px 12px !important;
|
| 232 |
border: none !important;
|
| 233 |
border-radius: 6px !important;
|
| 234 |
-
background: linear-gradient(135deg, #
|
| 235 |
color: white !important;
|
| 236 |
-
font-family: 'Segoe UI', Tahoma, sans-serif !important;
|
| 237 |
font-size: 12px !important;
|
| 238 |
font-weight: 600 !important;
|
| 239 |
cursor: pointer !important;
|
|
@@ -241,7 +241,7 @@
|
|
| 241 |
}
|
| 242 |
.bayan-il-tooltip-apply:hover {
|
| 243 |
transform: translateY(-1px) !important;
|
| 244 |
-
box-shadow: 0 2px 8px rgba(
|
| 245 |
}
|
| 246 |
|
| 247 |
.bayan-il-tooltip-ignore {
|
|
@@ -249,15 +249,15 @@
|
|
| 249 |
border: 1px solid #3a3a4d !important;
|
| 250 |
border-radius: 6px !important;
|
| 251 |
background: transparent !important;
|
| 252 |
-
color: #
|
| 253 |
-
font-family: 'Segoe UI', Tahoma, sans-serif !important;
|
| 254 |
font-size: 12px !important;
|
| 255 |
cursor: pointer !important;
|
| 256 |
transition: background 150ms ease !important;
|
| 257 |
}
|
| 258 |
.bayan-il-tooltip-ignore:hover {
|
| 259 |
-
background: #
|
| 260 |
-
color: #
|
| 261 |
}
|
| 262 |
|
| 263 |
/* ── Textarea overlay (transparent mirror) ── */
|
|
@@ -295,13 +295,13 @@
|
|
| 295 |
}
|
| 296 |
|
| 297 |
.bayan-il-fab[data-bayan-theme="light"] svg path {
|
| 298 |
-
fill: #
|
| 299 |
}
|
| 300 |
|
| 301 |
.bayan-il-tooltip[data-bayan-theme="light"] {
|
| 302 |
background: #ffffff !important;
|
| 303 |
border: 1px solid #e5e7eb !important;
|
| 304 |
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(
|
| 305 |
}
|
| 306 |
|
| 307 |
.bayan-il-tooltip[data-bayan-theme="light"] .bayan-il-tooltip-header {
|
|
@@ -341,40 +341,27 @@
|
|
| 341 |
══════════════════════════════════════════════ */
|
| 342 |
|
| 343 |
.bayan-il-modal-backdrop {
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
z-index: 2147483646 !important;
|
| 347 |
-
background: rgba(0, 0, 0, 0.55) !important;
|
| 348 |
-
backdrop-filter: blur(4px) !important;
|
| 349 |
-
opacity: 0;
|
| 350 |
-
transition: opacity 200ms ease !important;
|
| 351 |
-
display: none;
|
| 352 |
-
}
|
| 353 |
-
|
| 354 |
-
.bayan-il-modal-backdrop--visible {
|
| 355 |
-
display: block !important;
|
| 356 |
-
opacity: 1 !important;
|
| 357 |
}
|
| 358 |
|
| 359 |
.bayan-il-modal-panel {
|
| 360 |
position: fixed !important;
|
| 361 |
z-index: 2147483647 !important;
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
transform: translate(-50%, -50%) scale(0.95) !important;
|
| 365 |
-
width: 400px !important;
|
| 366 |
max-width: calc(100vw - 32px) !important;
|
| 367 |
max-height: 80vh !important;
|
| 368 |
-
background: #
|
| 369 |
-
border: 1px solid
|
| 370 |
-
border-radius:
|
| 371 |
-
box-shadow: 0
|
| 372 |
overflow: hidden !important;
|
| 373 |
display: flex !important;
|
| 374 |
flex-direction: column !important;
|
| 375 |
-
font-family: 'Segoe UI', Tahoma, Arial, sans-serif !important;
|
| 376 |
font-size: 13px !important;
|
| 377 |
-
color: #
|
| 378 |
direction: rtl !important;
|
| 379 |
opacity: 0;
|
| 380 |
transition: opacity 200ms ease, transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
|
|
@@ -382,7 +369,7 @@
|
|
| 382 |
|
| 383 |
.bayan-il-modal-panel--visible {
|
| 384 |
opacity: 1 !important;
|
| 385 |
-
transform:
|
| 386 |
}
|
| 387 |
|
| 388 |
/* ── Modal Header ── */
|
|
@@ -392,7 +379,7 @@
|
|
| 392 |
align-items: center !important;
|
| 393 |
justify-content: space-between !important;
|
| 394 |
padding: 12px 16px !important;
|
| 395 |
-
background: linear-gradient(135deg, #
|
| 396 |
border-bottom: 1px solid #2d2d3d !important;
|
| 397 |
flex-shrink: 0 !important;
|
| 398 |
}
|
|
@@ -406,7 +393,7 @@
|
|
| 406 |
.bayan-il-modal-title {
|
| 407 |
font-size: 16px !important;
|
| 408 |
font-weight: 700 !important;
|
| 409 |
-
background: linear-gradient(135deg, #
|
| 410 |
-webkit-background-clip: text !important;
|
| 411 |
-webkit-text-fill-color: transparent !important;
|
| 412 |
letter-spacing: -0.3px !important;
|
|
@@ -415,7 +402,7 @@
|
|
| 415 |
.bayan-il-modal-close {
|
| 416 |
background: none !important;
|
| 417 |
border: none !important;
|
| 418 |
-
color: #
|
| 419 |
cursor: pointer !important;
|
| 420 |
font-size: 18px !important;
|
| 421 |
padding: 4px !important;
|
|
@@ -425,40 +412,70 @@
|
|
| 425 |
}
|
| 426 |
|
| 427 |
.bayan-il-modal-close:hover {
|
| 428 |
-
color: #
|
| 429 |
background: rgba(255, 255, 255, 0.08) !important;
|
| 430 |
}
|
| 431 |
|
| 432 |
-
/* ── Modal
|
| 433 |
|
| 434 |
-
.bayan-il-modal-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
overflow-y: auto !important;
|
| 436 |
-
padding: 16px !important;
|
| 437 |
flex: 1 !important;
|
| 438 |
min-height: 0 !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
}
|
| 440 |
|
| 441 |
-
.bayan-il-modal-body::-webkit-scrollbar { width: 4px !important; }
|
| 442 |
-
.bayan-il-modal-body::-webkit-scrollbar-track { background: transparent !important; }
|
| 443 |
-
.bayan-il-modal-body::-webkit-scrollbar-thumb { background: #3a3a4d !important; border-radius: 4px !important; }
|
| 444 |
|
| 445 |
-
/* ──
|
| 446 |
|
| 447 |
-
.bayan-il-modal-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
display: flex !important;
|
|
|
|
| 449 |
align-items: center !important;
|
| 450 |
-
|
| 451 |
-
padding:
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
border: 1px solid #2d2d3d !important;
|
| 455 |
border-radius: 10px !important;
|
| 456 |
}
|
| 457 |
|
| 458 |
.bayan-il-modal-score-ring {
|
| 459 |
position: relative !important;
|
| 460 |
-
width:
|
| 461 |
-
height:
|
|
|
|
| 462 |
flex-shrink: 0 !important;
|
| 463 |
}
|
| 464 |
|
|
@@ -466,9 +483,10 @@
|
|
| 466 |
display: block !important;
|
| 467 |
width: 100% !important;
|
| 468 |
height: 100% !important;
|
|
|
|
| 469 |
}
|
| 470 |
|
| 471 |
-
.bayan-il-modal-score-ring
|
| 472 |
transition: stroke-dashoffset 800ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
|
| 473 |
}
|
| 474 |
|
|
@@ -478,32 +496,31 @@
|
|
| 478 |
display: flex !important;
|
| 479 |
align-items: center !important;
|
| 480 |
justify-content: center !important;
|
| 481 |
-
font-size:
|
| 482 |
-
font-weight:
|
| 483 |
-
color: #
|
| 484 |
-
}
|
| 485 |
-
|
| 486 |
-
.bayan-il-modal-score-meta {
|
| 487 |
-
flex: 1 !important;
|
| 488 |
-
min-width: 0 !important;
|
| 489 |
}
|
| 490 |
|
| 491 |
.bayan-il-modal-score-hint {
|
| 492 |
display: block !important;
|
|
|
|
| 493 |
font-size: 12px !important;
|
| 494 |
-
color: #
|
| 495 |
-
margin
|
| 496 |
}
|
| 497 |
|
| 498 |
-
|
|
|
|
|
|
|
| 499 |
display: flex !important;
|
|
|
|
| 500 |
gap: 10px !important;
|
| 501 |
flex-wrap: wrap !important;
|
| 502 |
}
|
| 503 |
|
| 504 |
.bayan-il-modal-count {
|
| 505 |
font-size: 11px !important;
|
| 506 |
-
color: #
|
| 507 |
display: flex !important;
|
| 508 |
align-items: center !important;
|
| 509 |
gap: 3px !important;
|
|
@@ -513,11 +530,18 @@
|
|
| 513 |
font-weight: 700 !important;
|
| 514 |
}
|
| 515 |
|
| 516 |
-
.bayan-il-modal-count--spelling strong { color: #
|
| 517 |
-
.bayan-il-modal-count--grammar strong { color: #
|
| 518 |
.bayan-il-modal-count--punctuation strong { color: #6BC98A !important; }
|
| 519 |
|
| 520 |
-
/* ── Suggestions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
|
| 522 |
.bayan-il-modal-sugg-header {
|
| 523 |
display: flex !important;
|
|
@@ -526,15 +550,12 @@
|
|
| 526 |
margin-bottom: 10px !important;
|
| 527 |
}
|
| 528 |
|
| 529 |
-
|
| 530 |
-
font-size: 12px !important;
|
| 531 |
-
font-weight: 600 !important;
|
| 532 |
-
color: #9898ad !important;
|
| 533 |
-
}
|
| 534 |
|
| 535 |
-
.bayan-il-
|
| 536 |
-
|
| 537 |
-
|
|
|
|
| 538 |
}
|
| 539 |
|
| 540 |
/* ── Suggestion Cards ── */
|
|
@@ -543,29 +564,29 @@
|
|
| 543 |
display: flex !important;
|
| 544 |
flex-direction: column !important;
|
| 545 |
gap: 8px !important;
|
| 546 |
-
|
|
|
|
| 547 |
}
|
| 548 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
.bayan-il-modal-card {
|
| 550 |
padding: 10px 12px !important;
|
| 551 |
-
background: #
|
| 552 |
-
border: 1px solid
|
| 553 |
-
border-right: 4px solid #6b6b80 !important;
|
| 554 |
border-radius: 10px !important;
|
| 555 |
cursor: pointer !important;
|
| 556 |
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1) !important;
|
| 557 |
}
|
| 558 |
|
| 559 |
.bayan-il-modal-card:hover {
|
| 560 |
-
border-color: #
|
| 561 |
-
background: #
|
| 562 |
-
|
| 563 |
}
|
| 564 |
|
| 565 |
-
.bayan-il-modal-card--spelling { border-right-color: #ef4444 !important; }
|
| 566 |
-
.bayan-il-modal-card--grammar { border-right-color: #f59e0b !important; }
|
| 567 |
-
.bayan-il-modal-card--punctuation { border-right-color: #6BC98A !important; }
|
| 568 |
-
|
| 569 |
.bayan-il-modal-card-badge {
|
| 570 |
display: inline-block !important;
|
| 571 |
padding: 2px 8px !important;
|
|
@@ -576,13 +597,13 @@
|
|
| 576 |
}
|
| 577 |
|
| 578 |
.bayan-il-modal-badge--spelling {
|
| 579 |
-
background: rgba(
|
| 580 |
-
color: #
|
| 581 |
}
|
| 582 |
|
| 583 |
.bayan-il-modal-badge--grammar {
|
| 584 |
-
background: rgba(
|
| 585 |
-
color: #
|
| 586 |
}
|
| 587 |
|
| 588 |
.bayan-il-modal-badge--punctuation {
|
|
@@ -601,18 +622,18 @@
|
|
| 601 |
|
| 602 |
.bayan-il-modal-card-original {
|
| 603 |
text-decoration: line-through !important;
|
| 604 |
-
text-decoration-color: rgba(
|
| 605 |
-
color: #
|
| 606 |
opacity: 0.75 !important;
|
| 607 |
}
|
| 608 |
|
| 609 |
.bayan-il-modal-card-arrow {
|
| 610 |
-
color: #
|
| 611 |
font-size: 12px !important;
|
| 612 |
}
|
| 613 |
|
| 614 |
.bayan-il-modal-card-fix {
|
| 615 |
-
color: #
|
| 616 |
font-weight: 600 !important;
|
| 617 |
}
|
| 618 |
|
|
@@ -627,39 +648,39 @@
|
|
| 627 |
border: 1px solid #3a3a4d !important;
|
| 628 |
border-radius: 20px !important;
|
| 629 |
background: transparent !important;
|
| 630 |
-
color: #
|
| 631 |
-
font-family: 'Segoe UI', Tahoma, sans-serif !important;
|
| 632 |
font-size: 11px !important;
|
| 633 |
cursor: pointer !important;
|
| 634 |
transition: all 200ms ease !important;
|
| 635 |
}
|
| 636 |
|
| 637 |
.bayan-il-modal-alt-chip:hover {
|
| 638 |
-
border-color: #
|
| 639 |
-
color: #
|
| 640 |
-
background: rgba(
|
| 641 |
}
|
| 642 |
|
| 643 |
.bayan-il-modal-alt-chip--main {
|
| 644 |
-
background: #
|
| 645 |
-
border-color: #
|
| 646 |
color: white !important;
|
| 647 |
}
|
| 648 |
|
| 649 |
.bayan-il-modal-alt-chip--main:hover {
|
| 650 |
-
background: #
|
| 651 |
color: white !important;
|
| 652 |
}
|
| 653 |
|
| 654 |
.bayan-il-modal-alt-chip--keep {
|
| 655 |
border-style: dashed !important;
|
| 656 |
-
color: #
|
| 657 |
opacity: 0.8 !important;
|
| 658 |
}
|
| 659 |
|
| 660 |
.bayan-il-modal-alt-chip--keep:hover {
|
| 661 |
opacity: 1 !important;
|
| 662 |
-
border-color: #
|
| 663 |
background: rgba(107, 107, 128, 0.1) !important;
|
| 664 |
}
|
| 665 |
|
|
@@ -667,17 +688,18 @@
|
|
| 667 |
|
| 668 |
.bayan-il-modal-apply-all {
|
| 669 |
width: 100% !important;
|
|
|
|
| 670 |
padding: 10px 18px !important;
|
| 671 |
border: none !important;
|
| 672 |
border-radius: 10px !important;
|
| 673 |
-
background: linear-gradient(135deg, #
|
| 674 |
color: white !important;
|
| 675 |
-
font-family: 'Segoe UI', Tahoma, sans-serif !important;
|
| 676 |
font-size: 13px !important;
|
| 677 |
font-weight: 600 !important;
|
| 678 |
cursor: pointer !important;
|
| 679 |
transition: transform 150ms ease, box-shadow 150ms ease !important;
|
| 680 |
-
box-shadow: 0 2px 8px rgba(
|
| 681 |
display: flex !important;
|
| 682 |
align-items: center !important;
|
| 683 |
justify-content: center !important;
|
|
@@ -686,7 +708,7 @@
|
|
| 686 |
|
| 687 |
.bayan-il-modal-apply-all:hover {
|
| 688 |
transform: translateY(-1px) !important;
|
| 689 |
-
box-shadow: 0 4px 16px rgba(
|
| 690 |
}
|
| 691 |
|
| 692 |
.bayan-il-modal-apply-all:active {
|
|
@@ -698,7 +720,7 @@
|
|
| 698 |
.bayan-il-modal-empty {
|
| 699 |
text-align: center !important;
|
| 700 |
padding: 24px 16px !important;
|
| 701 |
-
color: #
|
| 702 |
font-size: 13px !important;
|
| 703 |
}
|
| 704 |
|
|
@@ -712,111 +734,458 @@
|
|
| 712 |
Modal — Light Theme Overrides
|
| 713 |
══════════════════════════════════════════════ */
|
| 714 |
|
| 715 |
-
.bayan-il-modal-backdrop[data-bayan-theme="light"] {
|
| 716 |
-
background: rgba(0, 0, 0, 0.3) !important;
|
| 717 |
-
}
|
| 718 |
-
|
| 719 |
.bayan-il-modal-panel[data-bayan-theme="light"] {
|
| 720 |
-
background: #
|
| 721 |
-
border-color:
|
| 722 |
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(
|
| 723 |
-
color: #
|
| 724 |
}
|
| 725 |
|
| 726 |
-
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-
|
| 727 |
-
background: linear-gradient(135deg, #
|
| 728 |
-
border-bottom-color:
|
| 729 |
}
|
| 730 |
|
| 731 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-close {
|
| 732 |
-
color: #
|
| 733 |
}
|
| 734 |
|
| 735 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-close:hover {
|
| 736 |
-
color: #
|
| 737 |
background: rgba(0, 0, 0, 0.05) !important;
|
| 738 |
}
|
| 739 |
|
| 740 |
-
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-body::-webkit-scrollbar-thumb {
|
| 741 |
-
background:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
}
|
| 743 |
|
| 744 |
-
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-score {
|
| 745 |
-
background: #
|
| 746 |
-
border-color:
|
| 747 |
}
|
| 748 |
|
| 749 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-score-value {
|
| 750 |
-
color: #
|
| 751 |
}
|
| 752 |
|
| 753 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-score-hint {
|
| 754 |
-
color: #
|
| 755 |
}
|
| 756 |
|
| 757 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-count {
|
| 758 |
-
color: #
|
| 759 |
}
|
| 760 |
|
| 761 |
-
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-sugg-
|
| 762 |
-
|
|
|
|
| 763 |
}
|
| 764 |
|
| 765 |
-
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-
|
| 766 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
}
|
| 768 |
|
| 769 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card {
|
| 770 |
-
background: #
|
| 771 |
-
border-color:
|
| 772 |
}
|
| 773 |
|
| 774 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card:hover {
|
| 775 |
-
background: #
|
| 776 |
-
border-color: #
|
|
|
|
| 777 |
}
|
| 778 |
|
| 779 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card-original {
|
| 780 |
-
color: #
|
| 781 |
}
|
| 782 |
|
| 783 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card-arrow {
|
| 784 |
-
color: #
|
| 785 |
}
|
| 786 |
|
| 787 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card-fix {
|
| 788 |
-
color: #
|
| 789 |
}
|
| 790 |
|
| 791 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip {
|
| 792 |
-
border-color:
|
| 793 |
-
color: #
|
| 794 |
background: transparent !important;
|
| 795 |
}
|
| 796 |
|
| 797 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip:hover {
|
| 798 |
-
border-color: #
|
| 799 |
-
color: #
|
| 800 |
-
background: rgba(
|
| 801 |
}
|
| 802 |
|
| 803 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip--main {
|
| 804 |
-
background: #
|
| 805 |
-
border-color: #
|
| 806 |
color: white !important;
|
| 807 |
}
|
| 808 |
|
| 809 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip--keep {
|
| 810 |
-
color: #
|
| 811 |
-
border-color:
|
| 812 |
}
|
| 813 |
|
| 814 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip--keep:hover {
|
| 815 |
-
border-color: #
|
| 816 |
background: rgba(0, 0, 0, 0.03) !important;
|
| 817 |
}
|
| 818 |
|
| 819 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-empty {
|
| 820 |
-
color: #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 821 |
}
|
| 822 |
|
|
|
|
| 7 |
/* ── Error highlights (contenteditable + overlay) ── */
|
| 8 |
|
| 9 |
.bayan-il-spelling {
|
| 10 |
+
background: rgba(232, 138, 138, 0.12) !important;
|
| 11 |
+
border-bottom: 2px solid #E88A8A !important;
|
| 12 |
border-radius: 2px !important;
|
| 13 |
cursor: pointer !important;
|
| 14 |
transition: background 150ms ease !important;
|
| 15 |
}
|
| 16 |
.bayan-il-spelling:hover {
|
| 17 |
+
background: rgba(232, 138, 138, 0.25) !important;
|
| 18 |
}
|
| 19 |
|
| 20 |
.bayan-il-grammar {
|
| 21 |
+
background: rgba(228, 179, 90, 0.12) !important;
|
| 22 |
+
border-bottom: 2px solid #E4B35A !important;
|
| 23 |
border-radius: 2px !important;
|
| 24 |
cursor: pointer !important;
|
| 25 |
transition: background 150ms ease !important;
|
| 26 |
}
|
| 27 |
.bayan-il-grammar:hover {
|
| 28 |
+
background: rgba(228, 179, 90, 0.25) !important;
|
| 29 |
}
|
| 30 |
|
| 31 |
.bayan-il-punctuation {
|
|
|
|
| 49 |
display: flex;
|
| 50 |
align-items: center;
|
| 51 |
justify-content: center;
|
| 52 |
+
background: #1A1D26 !important;
|
| 53 |
border: 1px solid #3a3a4d !important;
|
| 54 |
border-radius: 50% !important;
|
| 55 |
cursor: pointer !important;
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
.bayan-il-fab:hover {
|
| 71 |
+
border-color: #6BA3E0 !important;
|
| 72 |
+
box-shadow: 0 2px 14px rgba(107, 163, 224, 0.3) !important;
|
| 73 |
}
|
| 74 |
|
| 75 |
/* ── Badge (on FAB) ── */
|
|
|
|
| 89 |
border-radius: 8px !important;
|
| 90 |
padding: 0 4px !important;
|
| 91 |
line-height: 1 !important;
|
| 92 |
+
border: 1.5px solid #1A1D26 !important;
|
| 93 |
color: white !important;
|
| 94 |
+
background: #8A939F !important;
|
| 95 |
}
|
| 96 |
|
| 97 |
.bayan-il-badge--clean {
|
| 98 |
+
background: #6BC98A !important;
|
| 99 |
}
|
| 100 |
|
| 101 |
.bayan-il-badge--errors {
|
| 102 |
+
background: #E88A8A !important;
|
| 103 |
animation: bayan-il-pulse 600ms ease-out !important;
|
| 104 |
}
|
| 105 |
|
| 106 |
.bayan-il-badge--analyzing {
|
| 107 |
+
background: #6BA3E0 !important;
|
| 108 |
animation: bayan-il-spin 1s linear infinite !important;
|
| 109 |
}
|
| 110 |
|
| 111 |
.bayan-il-badge--paused {
|
| 112 |
+
background: #8A939F !important;
|
| 113 |
opacity: 0.7 !important;
|
| 114 |
}
|
| 115 |
|
|
|
|
| 132 |
z-index: 2147483647 !important;
|
| 133 |
min-width: 200px;
|
| 134 |
max-width: 320px;
|
| 135 |
+
background: #1A1D26 !important;
|
| 136 |
border: 1px solid #3a3a4d !important;
|
| 137 |
border-radius: 10px !important;
|
| 138 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(107, 163, 224, 0.1) !important;
|
| 139 |
overflow: hidden;
|
| 140 |
+
font-family: 'Cairo', 'Segoe UI', Tahoma, Arial, sans-serif !important;
|
| 141 |
font-size: 13px !important;
|
| 142 |
+
color: #ECEEF2 !important;
|
| 143 |
opacity: 0;
|
| 144 |
transform: translateY(4px);
|
| 145 |
transition: opacity 150ms ease, transform 150ms ease !important;
|
|
|
|
| 155 |
align-items: center !important;
|
| 156 |
justify-content: space-between !important;
|
| 157 |
padding: 8px 12px !important;
|
| 158 |
+
background: #242833 !important;
|
| 159 |
border-bottom: 1px solid #2d2d3d !important;
|
| 160 |
}
|
| 161 |
|
|
|
|
| 168 |
}
|
| 169 |
|
| 170 |
.bayan-il-badge-spelling {
|
| 171 |
+
background: rgba(232, 138, 138, 0.15) !important;
|
| 172 |
+
color: #E88A8A !important;
|
| 173 |
}
|
| 174 |
.bayan-il-badge-grammar {
|
| 175 |
+
background: rgba(228, 179, 90, 0.15) !important;
|
| 176 |
+
color: #E4B35A !important;
|
| 177 |
}
|
| 178 |
.bayan-il-badge-punctuation {
|
| 179 |
background: rgba(107, 201, 138, 0.15) !important;
|
|
|
|
| 183 |
.bayan-il-tooltip-close {
|
| 184 |
background: none !important;
|
| 185 |
border: none !important;
|
| 186 |
+
color: #8A939F !important;
|
| 187 |
cursor: pointer !important;
|
| 188 |
font-size: 14px !important;
|
| 189 |
padding: 2px !important;
|
| 190 |
line-height: 1 !important;
|
| 191 |
}
|
| 192 |
.bayan-il-tooltip-close:hover {
|
| 193 |
+
color: #ECEEF2 !important;
|
| 194 |
}
|
| 195 |
|
| 196 |
.bayan-il-tooltip-body {
|
|
|
|
| 203 |
}
|
| 204 |
|
| 205 |
.bayan-il-tooltip-original {
|
| 206 |
+
color: #E88A8A !important;
|
| 207 |
text-decoration: line-through !important;
|
| 208 |
+
text-decoration-color: rgba(232, 138, 138, 0.4) !important;
|
| 209 |
}
|
| 210 |
|
| 211 |
.bayan-il-tooltip-arrow {
|
| 212 |
+
color: #8A939F !important;
|
| 213 |
font-size: 12px !important;
|
| 214 |
}
|
| 215 |
|
| 216 |
.bayan-il-tooltip-correction {
|
| 217 |
+
color: #6BC98A !important;
|
| 218 |
font-weight: 600 !important;
|
| 219 |
}
|
| 220 |
|
|
|
|
| 223 |
gap: 6px !important;
|
| 224 |
padding: 8px 12px !important;
|
| 225 |
border-top: 1px solid #2d2d3d !important;
|
| 226 |
+
background: #242833 !important;
|
| 227 |
}
|
| 228 |
|
| 229 |
.bayan-il-tooltip-apply {
|
|
|
|
| 231 |
padding: 5px 12px !important;
|
| 232 |
border: none !important;
|
| 233 |
border-radius: 6px !important;
|
| 234 |
+
background: linear-gradient(135deg, #6BA3E0, #5A8FCA) !important;
|
| 235 |
color: white !important;
|
| 236 |
+
font-family: 'Cairo', 'Segoe UI', Tahoma, sans-serif !important;
|
| 237 |
font-size: 12px !important;
|
| 238 |
font-weight: 600 !important;
|
| 239 |
cursor: pointer !important;
|
|
|
|
| 241 |
}
|
| 242 |
.bayan-il-tooltip-apply:hover {
|
| 243 |
transform: translateY(-1px) !important;
|
| 244 |
+
box-shadow: 0 2px 8px rgba(107, 163, 224, 0.3) !important;
|
| 245 |
}
|
| 246 |
|
| 247 |
.bayan-il-tooltip-ignore {
|
|
|
|
| 249 |
border: 1px solid #3a3a4d !important;
|
| 250 |
border-radius: 6px !important;
|
| 251 |
background: transparent !important;
|
| 252 |
+
color: #B4BBC6 !important;
|
| 253 |
+
font-family: 'Cairo', 'Segoe UI', Tahoma, sans-serif !important;
|
| 254 |
font-size: 12px !important;
|
| 255 |
cursor: pointer !important;
|
| 256 |
transition: background 150ms ease !important;
|
| 257 |
}
|
| 258 |
.bayan-il-tooltip-ignore:hover {
|
| 259 |
+
background: #2C3040 !important;
|
| 260 |
+
color: #ECEEF2 !important;
|
| 261 |
}
|
| 262 |
|
| 263 |
/* ── Textarea overlay (transparent mirror) ── */
|
|
|
|
| 295 |
}
|
| 296 |
|
| 297 |
.bayan-il-fab[data-bayan-theme="light"] svg path {
|
| 298 |
+
fill: #6BA3E0 !important;
|
| 299 |
}
|
| 300 |
|
| 301 |
.bayan-il-tooltip[data-bayan-theme="light"] {
|
| 302 |
background: #ffffff !important;
|
| 303 |
border: 1px solid #e5e7eb !important;
|
| 304 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(107, 163, 224, 0.1) !important;
|
| 305 |
}
|
| 306 |
|
| 307 |
.bayan-il-tooltip[data-bayan-theme="light"] .bayan-il-tooltip-header {
|
|
|
|
| 341 |
══════════════════════════════════════════════ */
|
| 342 |
|
| 343 |
.bayan-il-modal-backdrop {
|
| 344 |
+
display: none !important;
|
| 345 |
+
pointer-events: none !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
}
|
| 347 |
|
| 348 |
.bayan-il-modal-panel {
|
| 349 |
position: fixed !important;
|
| 350 |
z-index: 2147483647 !important;
|
| 351 |
+
transform: translateY(4px);
|
| 352 |
+
width: 360px !important;
|
|
|
|
|
|
|
| 353 |
max-width: calc(100vw - 32px) !important;
|
| 354 |
max-height: 80vh !important;
|
| 355 |
+
background: #12141A !important;
|
| 356 |
+
border: 1px solid rgba(236, 238, 242, 0.09) !important;
|
| 357 |
+
border-radius: 1rem !important;
|
| 358 |
+
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.38) !important;
|
| 359 |
overflow: hidden !important;
|
| 360 |
display: flex !important;
|
| 361 |
flex-direction: column !important;
|
| 362 |
+
font-family: 'Cairo', 'Segoe UI', Tahoma, Arial, sans-serif !important;
|
| 363 |
font-size: 13px !important;
|
| 364 |
+
color: #ECEEF2 !important;
|
| 365 |
direction: rtl !important;
|
| 366 |
opacity: 0;
|
| 367 |
transition: opacity 200ms ease, transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
|
|
|
|
| 369 |
|
| 370 |
.bayan-il-modal-panel--visible {
|
| 371 |
opacity: 1 !important;
|
| 372 |
+
transform: translateY(0) !important;
|
| 373 |
}
|
| 374 |
|
| 375 |
/* ── Modal Header ── */
|
|
|
|
| 379 |
align-items: center !important;
|
| 380 |
justify-content: space-between !important;
|
| 381 |
padding: 12px 16px !important;
|
| 382 |
+
background: linear-gradient(135deg, #1A1D26 0%, rgba(107, 163, 224, 0.08) 100%) !important;
|
| 383 |
border-bottom: 1px solid #2d2d3d !important;
|
| 384 |
flex-shrink: 0 !important;
|
| 385 |
}
|
|
|
|
| 393 |
.bayan-il-modal-title {
|
| 394 |
font-size: 16px !important;
|
| 395 |
font-weight: 700 !important;
|
| 396 |
+
background: linear-gradient(135deg, #8BB8E8, #6BA3E0) !important;
|
| 397 |
-webkit-background-clip: text !important;
|
| 398 |
-webkit-text-fill-color: transparent !important;
|
| 399 |
letter-spacing: -0.3px !important;
|
|
|
|
| 402 |
.bayan-il-modal-close {
|
| 403 |
background: none !important;
|
| 404 |
border: none !important;
|
| 405 |
+
color: #8A939F !important;
|
| 406 |
cursor: pointer !important;
|
| 407 |
font-size: 18px !important;
|
| 408 |
padding: 4px !important;
|
|
|
|
| 412 |
}
|
| 413 |
|
| 414 |
.bayan-il-modal-close:hover {
|
| 415 |
+
color: #ECEEF2 !important;
|
| 416 |
background: rgba(255, 255, 255, 0.08) !important;
|
| 417 |
}
|
| 418 |
|
| 419 |
+
/* ── Modal Top Bar ── */
|
| 420 |
|
| 421 |
+
.bayan-il-modal-top-bar {
|
| 422 |
+
display: flex !important;
|
| 423 |
+
align-items: center !important;
|
| 424 |
+
justify-content: space-between !important;
|
| 425 |
+
padding: 12px 16px !important;
|
| 426 |
+
background: linear-gradient(135deg, #1A1D26 0%, rgba(107, 163, 224, 0.08) 100%) !important;
|
| 427 |
+
border-bottom: 1px solid #2d2d3d !important;
|
| 428 |
+
flex-shrink: 0 !important;
|
| 429 |
+
cursor: grab !important;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.bayan-il-modal-top-bar:active {
|
| 433 |
+
cursor: grabbing !important;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
/* ── Modal Body Scroll ── */
|
| 437 |
+
|
| 438 |
+
.bayan-il-modal-body-scroll {
|
| 439 |
overflow-y: auto !important;
|
|
|
|
| 440 |
flex: 1 !important;
|
| 441 |
min-height: 0 !important;
|
| 442 |
+
display: flex !important;
|
| 443 |
+
flex-direction: column !important;
|
| 444 |
+
gap: 0.75rem !important;
|
| 445 |
+
padding: 0.75rem !important;
|
| 446 |
}
|
| 447 |
|
| 448 |
+
.bayan-il-modal-body-scroll::-webkit-scrollbar { width: 4px !important; }
|
| 449 |
+
.bayan-il-modal-body-scroll::-webkit-scrollbar-track { background: transparent !important; }
|
| 450 |
+
.bayan-il-modal-body-scroll::-webkit-scrollbar-thumb { background: #3a3a4d !important; border-radius: 4px !important; }
|
| 451 |
|
| 452 |
+
/* ── Section Title ── */
|
| 453 |
|
| 454 |
+
.bayan-il-modal-section-title {
|
| 455 |
+
font-size: 13px !important;
|
| 456 |
+
font-weight: 600 !important;
|
| 457 |
+
margin: 0 0 10px 0 !important;
|
| 458 |
+
color: #B4BBC6 !important;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
/* ── Score Card ── */
|
| 462 |
+
|
| 463 |
+
.bayan-il-modal-score-card {
|
| 464 |
display: flex !important;
|
| 465 |
+
flex-direction: column !important;
|
| 466 |
align-items: center !important;
|
| 467 |
+
text-align: center !important;
|
| 468 |
+
padding: 16px 12px !important;
|
| 469 |
+
background: #1A1D26 !important;
|
| 470 |
+
border: 1px solid rgba(236, 238, 242, 0.09) !important;
|
|
|
|
| 471 |
border-radius: 10px !important;
|
| 472 |
}
|
| 473 |
|
| 474 |
.bayan-il-modal-score-ring {
|
| 475 |
position: relative !important;
|
| 476 |
+
width: 120px !important;
|
| 477 |
+
height: 120px !important;
|
| 478 |
+
margin: 0 auto 8px !important;
|
| 479 |
flex-shrink: 0 !important;
|
| 480 |
}
|
| 481 |
|
|
|
|
| 483 |
display: block !important;
|
| 484 |
width: 100% !important;
|
| 485 |
height: 100% !important;
|
| 486 |
+
transform: rotate(-90deg) !important;
|
| 487 |
}
|
| 488 |
|
| 489 |
+
.bayan-il-modal-score-ring .bayan-il-score-circle {
|
| 490 |
transition: stroke-dashoffset 800ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
|
| 491 |
}
|
| 492 |
|
|
|
|
| 496 |
display: flex !important;
|
| 497 |
align-items: center !important;
|
| 498 |
justify-content: center !important;
|
| 499 |
+
font-size: 28px !important;
|
| 500 |
+
font-weight: 700 !important;
|
| 501 |
+
color: #8BB8E8 !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
}
|
| 503 |
|
| 504 |
.bayan-il-modal-score-hint {
|
| 505 |
display: block !important;
|
| 506 |
+
text-align: center !important;
|
| 507 |
font-size: 12px !important;
|
| 508 |
+
color: #B4BBC6 !important;
|
| 509 |
+
margin: 0 0 8px 0 !important;
|
| 510 |
}
|
| 511 |
|
| 512 |
+
/* ── Counts Row ── */
|
| 513 |
+
|
| 514 |
+
.bayan-il-modal-counts-row {
|
| 515 |
display: flex !important;
|
| 516 |
+
justify-content: center !important;
|
| 517 |
gap: 10px !important;
|
| 518 |
flex-wrap: wrap !important;
|
| 519 |
}
|
| 520 |
|
| 521 |
.bayan-il-modal-count {
|
| 522 |
font-size: 11px !important;
|
| 523 |
+
color: #8A939F !important;
|
| 524 |
display: flex !important;
|
| 525 |
align-items: center !important;
|
| 526 |
gap: 3px !important;
|
|
|
|
| 530 |
font-weight: 700 !important;
|
| 531 |
}
|
| 532 |
|
| 533 |
+
.bayan-il-modal-count--spelling strong { color: #E88A8A !important; }
|
| 534 |
+
.bayan-il-modal-count--grammar strong { color: #E4B35A !important; }
|
| 535 |
.bayan-il-modal-count--punctuation strong { color: #6BC98A !important; }
|
| 536 |
|
| 537 |
+
/* ── Suggestions Card ── */
|
| 538 |
+
|
| 539 |
+
.bayan-il-modal-sugg-card {
|
| 540 |
+
padding: 16px 12px !important;
|
| 541 |
+
background: #1A1D26 !important;
|
| 542 |
+
border: 1px solid rgba(236, 238, 242, 0.09) !important;
|
| 543 |
+
border-radius: 10px !important;
|
| 544 |
+
}
|
| 545 |
|
| 546 |
.bayan-il-modal-sugg-header {
|
| 547 |
display: flex !important;
|
|
|
|
| 550 |
margin-bottom: 10px !important;
|
| 551 |
}
|
| 552 |
|
| 553 |
+
/* ── Header Divider ── */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
|
| 555 |
+
.bayan-il-header-divider {
|
| 556 |
+
width: 1px !important;
|
| 557 |
+
height: 20px !important;
|
| 558 |
+
background: #3a3a4d !important;
|
| 559 |
}
|
| 560 |
|
| 561 |
/* ── Suggestion Cards ── */
|
|
|
|
| 564 |
display: flex !important;
|
| 565 |
flex-direction: column !important;
|
| 566 |
gap: 8px !important;
|
| 567 |
+
max-height: 250px !important;
|
| 568 |
+
overflow-y: auto !important;
|
| 569 |
}
|
| 570 |
|
| 571 |
+
.bayan-il-modal-cards::-webkit-scrollbar { width: 4px !important; }
|
| 572 |
+
.bayan-il-modal-cards::-webkit-scrollbar-track { background: transparent !important; }
|
| 573 |
+
.bayan-il-modal-cards::-webkit-scrollbar-thumb { background: #3a3a4d !important; border-radius: 4px !important; }
|
| 574 |
+
|
| 575 |
.bayan-il-modal-card {
|
| 576 |
padding: 10px 12px !important;
|
| 577 |
+
background: #1A1D26 !important;
|
| 578 |
+
border: 1px solid rgba(236, 238, 242, 0.09) !important;
|
|
|
|
| 579 |
border-radius: 10px !important;
|
| 580 |
cursor: pointer !important;
|
| 581 |
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1) !important;
|
| 582 |
}
|
| 583 |
|
| 584 |
.bayan-il-modal-card:hover {
|
| 585 |
+
border-color: #6BA3E0 !important;
|
| 586 |
+
background: #242833 !important;
|
| 587 |
+
box-shadow: 0 0 0 1px rgba(107, 163, 224, 0.25) !important;
|
| 588 |
}
|
| 589 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
.bayan-il-modal-card-badge {
|
| 591 |
display: inline-block !important;
|
| 592 |
padding: 2px 8px !important;
|
|
|
|
| 597 |
}
|
| 598 |
|
| 599 |
.bayan-il-modal-badge--spelling {
|
| 600 |
+
background: rgba(232, 138, 138, 0.15) !important;
|
| 601 |
+
color: #E88A8A !important;
|
| 602 |
}
|
| 603 |
|
| 604 |
.bayan-il-modal-badge--grammar {
|
| 605 |
+
background: rgba(228, 179, 90, 0.15) !important;
|
| 606 |
+
color: #E4B35A !important;
|
| 607 |
}
|
| 608 |
|
| 609 |
.bayan-il-modal-badge--punctuation {
|
|
|
|
| 622 |
|
| 623 |
.bayan-il-modal-card-original {
|
| 624 |
text-decoration: line-through !important;
|
| 625 |
+
text-decoration-color: rgba(232, 138, 138, 0.4) !important;
|
| 626 |
+
color: #E88A8A !important;
|
| 627 |
opacity: 0.75 !important;
|
| 628 |
}
|
| 629 |
|
| 630 |
.bayan-il-modal-card-arrow {
|
| 631 |
+
color: #8A939F !important;
|
| 632 |
font-size: 12px !important;
|
| 633 |
}
|
| 634 |
|
| 635 |
.bayan-il-modal-card-fix {
|
| 636 |
+
color: #6BC98A !important;
|
| 637 |
font-weight: 600 !important;
|
| 638 |
}
|
| 639 |
|
|
|
|
| 648 |
border: 1px solid #3a3a4d !important;
|
| 649 |
border-radius: 20px !important;
|
| 650 |
background: transparent !important;
|
| 651 |
+
color: #B4BBC6 !important;
|
| 652 |
+
font-family: 'Cairo', 'Segoe UI', Tahoma, sans-serif !important;
|
| 653 |
font-size: 11px !important;
|
| 654 |
cursor: pointer !important;
|
| 655 |
transition: all 200ms ease !important;
|
| 656 |
}
|
| 657 |
|
| 658 |
.bayan-il-modal-alt-chip:hover {
|
| 659 |
+
border-color: #6BA3E0 !important;
|
| 660 |
+
color: #ECEEF2 !important;
|
| 661 |
+
background: rgba(107, 163, 224, 0.1) !important;
|
| 662 |
}
|
| 663 |
|
| 664 |
.bayan-il-modal-alt-chip--main {
|
| 665 |
+
background: #6BA3E0 !important;
|
| 666 |
+
border-color: #6BA3E0 !important;
|
| 667 |
color: white !important;
|
| 668 |
}
|
| 669 |
|
| 670 |
.bayan-il-modal-alt-chip--main:hover {
|
| 671 |
+
background: #5A8FCA !important;
|
| 672 |
color: white !important;
|
| 673 |
}
|
| 674 |
|
| 675 |
.bayan-il-modal-alt-chip--keep {
|
| 676 |
border-style: dashed !important;
|
| 677 |
+
color: #8A939F !important;
|
| 678 |
opacity: 0.8 !important;
|
| 679 |
}
|
| 680 |
|
| 681 |
.bayan-il-modal-alt-chip--keep:hover {
|
| 682 |
opacity: 1 !important;
|
| 683 |
+
border-color: #8A939F !important;
|
| 684 |
background: rgba(107, 107, 128, 0.1) !important;
|
| 685 |
}
|
| 686 |
|
|
|
|
| 688 |
|
| 689 |
.bayan-il-modal-apply-all {
|
| 690 |
width: 100% !important;
|
| 691 |
+
margin-top: 10px !important;
|
| 692 |
padding: 10px 18px !important;
|
| 693 |
border: none !important;
|
| 694 |
border-radius: 10px !important;
|
| 695 |
+
background: linear-gradient(135deg, #6BA3E0, #5A8FCA) !important;
|
| 696 |
color: white !important;
|
| 697 |
+
font-family: 'Cairo', 'Segoe UI', Tahoma, sans-serif !important;
|
| 698 |
font-size: 13px !important;
|
| 699 |
font-weight: 600 !important;
|
| 700 |
cursor: pointer !important;
|
| 701 |
transition: transform 150ms ease, box-shadow 150ms ease !important;
|
| 702 |
+
box-shadow: 0 2px 8px rgba(107, 163, 224, 0.3) !important;
|
| 703 |
display: flex !important;
|
| 704 |
align-items: center !important;
|
| 705 |
justify-content: center !important;
|
|
|
|
| 708 |
|
| 709 |
.bayan-il-modal-apply-all:hover {
|
| 710 |
transform: translateY(-1px) !important;
|
| 711 |
+
box-shadow: 0 4px 16px rgba(107, 163, 224, 0.45) !important;
|
| 712 |
}
|
| 713 |
|
| 714 |
.bayan-il-modal-apply-all:active {
|
|
|
|
| 720 |
.bayan-il-modal-empty {
|
| 721 |
text-align: center !important;
|
| 722 |
padding: 24px 16px !important;
|
| 723 |
+
color: #8A939F !important;
|
| 724 |
font-size: 13px !important;
|
| 725 |
}
|
| 726 |
|
|
|
|
| 734 |
Modal — Light Theme Overrides
|
| 735 |
══════════════════════════════════════════════ */
|
| 736 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
.bayan-il-modal-panel[data-bayan-theme="light"] {
|
| 738 |
+
background: #F3F1EC !important;
|
| 739 |
+
border-color: rgba(26, 29, 33, 0.1) !important;
|
| 740 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(43, 108, 184, 0.08) !important;
|
| 741 |
+
color: #1A1D21 !important;
|
| 742 |
}
|
| 743 |
|
| 744 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-top-bar {
|
| 745 |
+
background: linear-gradient(135deg, #FAF9F6 0%, rgba(43, 108, 184, 0.05) 100%) !important;
|
| 746 |
+
border-bottom-color: rgba(26, 29, 33, 0.1) !important;
|
| 747 |
}
|
| 748 |
|
| 749 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-close {
|
| 750 |
+
color: #5A6472 !important;
|
| 751 |
}
|
| 752 |
|
| 753 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-close:hover {
|
| 754 |
+
color: #1A1D21 !important;
|
| 755 |
background: rgba(0, 0, 0, 0.05) !important;
|
| 756 |
}
|
| 757 |
|
| 758 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-body-scroll::-webkit-scrollbar-thumb {
|
| 759 |
+
background: rgba(26, 29, 33, 0.18) !important;
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-section-title {
|
| 763 |
+
color: #3A424E !important;
|
| 764 |
}
|
| 765 |
|
| 766 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-score-card {
|
| 767 |
+
background: #FAF9F6 !important;
|
| 768 |
+
border-color: rgba(26, 29, 33, 0.1) !important;
|
| 769 |
}
|
| 770 |
|
| 771 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-score-value {
|
| 772 |
+
color: #6B57A8 !important;
|
| 773 |
}
|
| 774 |
|
| 775 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-score-hint {
|
| 776 |
+
color: #3A424E !important;
|
| 777 |
}
|
| 778 |
|
| 779 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-count {
|
| 780 |
+
color: #5A6472 !important;
|
| 781 |
}
|
| 782 |
|
| 783 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-sugg-card {
|
| 784 |
+
background: #FAF9F6 !important;
|
| 785 |
+
border-color: rgba(26, 29, 33, 0.1) !important;
|
| 786 |
}
|
| 787 |
|
| 788 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-cards::-webkit-scrollbar-thumb {
|
| 789 |
+
background: #d1d5db !important;
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-header-divider {
|
| 793 |
+
background: rgba(26, 29, 33, 0.18) !important;
|
| 794 |
}
|
| 795 |
|
| 796 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card {
|
| 797 |
+
background: #FAF9F6 !important;
|
| 798 |
+
border-color: rgba(26, 29, 33, 0.1) !important;
|
| 799 |
}
|
| 800 |
|
| 801 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card:hover {
|
| 802 |
+
background: #EFEBE4 !important;
|
| 803 |
+
border-color: #2B6CB8 !important;
|
| 804 |
+
box-shadow: 0 0 0 1px rgba(43, 108, 184, 0.32) !important;
|
| 805 |
}
|
| 806 |
|
| 807 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card-original {
|
| 808 |
+
color: #C53030 !important;
|
| 809 |
}
|
| 810 |
|
| 811 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card-arrow {
|
| 812 |
+
color: #5A6472 !important;
|
| 813 |
}
|
| 814 |
|
| 815 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card-fix {
|
| 816 |
+
color: #2F8554 !important;
|
| 817 |
}
|
| 818 |
|
| 819 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip {
|
| 820 |
+
border-color: rgba(26, 29, 33, 0.18) !important;
|
| 821 |
+
color: #5A6472 !important;
|
| 822 |
background: transparent !important;
|
| 823 |
}
|
| 824 |
|
| 825 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip:hover {
|
| 826 |
+
border-color: #2B6CB8 !important;
|
| 827 |
+
color: #2B6CB8 !important;
|
| 828 |
+
background: rgba(43, 108, 184, 0.06) !important;
|
| 829 |
}
|
| 830 |
|
| 831 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip--main {
|
| 832 |
+
background: #2B6CB8 !important;
|
| 833 |
+
border-color: #2B6CB8 !important;
|
| 834 |
color: white !important;
|
| 835 |
}
|
| 836 |
|
| 837 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip--keep {
|
| 838 |
+
color: #5A6472 !important;
|
| 839 |
+
border-color: rgba(26, 29, 33, 0.18) !important;
|
| 840 |
}
|
| 841 |
|
| 842 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip--keep:hover {
|
| 843 |
+
border-color: #5A6472 !important;
|
| 844 |
background: rgba(0, 0, 0, 0.03) !important;
|
| 845 |
}
|
| 846 |
|
| 847 |
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-empty {
|
| 848 |
+
color: #5A6472 !important;
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-score-ring svg circle:first-child {
|
| 852 |
+
stroke: rgba(26, 29, 33, 0.1) !important;
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-count--spelling strong { color: #C53030 !important; }
|
| 856 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-count--grammar strong { color: #B7791F !important; }
|
| 857 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-count--punctuation strong { color: #2F8554 !important; }
|
| 858 |
+
|
| 859 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-apply-all {
|
| 860 |
+
background: linear-gradient(135deg, #2B6CB8, #1A5CA0) !important;
|
| 861 |
+
box-shadow: 0 2px 8px rgba(43, 108, 184, 0.3) !important;
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-apply-all:hover {
|
| 865 |
+
box-shadow: 0 4px 16px rgba(43, 108, 184, 0.45) !important;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
.bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-cards::-webkit-scrollbar-thumb {
|
| 869 |
+
background: rgba(26, 29, 33, 0.18) !important;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
|
| 873 |
+
|
| 874 |
+
/* ── UI Sync CSS ── */
|
| 875 |
+
|
| 876 |
+
.bayan-il-suggestion-popover {
|
| 877 |
+
position: absolute;
|
| 878 |
+
z-index: 2147483647 !important;
|
| 879 |
+
background: #1A1D26 !important;
|
| 880 |
+
border: 1px solid rgba(236, 238, 242, 0.16) !important;
|
| 881 |
+
border-radius: 1rem !important;
|
| 882 |
+
padding: 1rem !important;
|
| 883 |
+
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.38) !important;
|
| 884 |
+
width: 320px !important;
|
| 885 |
+
display: none;
|
| 886 |
+
opacity: 0;
|
| 887 |
+
transform: translateY(4px);
|
| 888 |
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
| 889 |
+
font-family: 'Cairo', 'Tajawal', 'Noto Sans Arabic', sans-serif !important;
|
| 890 |
+
direction: rtl;
|
| 891 |
+
color: #ECEEF2;
|
| 892 |
+
}
|
| 893 |
+
.bayan-il-suggestion-popover.bayan-il-show {
|
| 894 |
+
display: block !important;
|
| 895 |
+
opacity: 1 !important;
|
| 896 |
+
transform: translateY(0) !important;
|
| 897 |
+
}
|
| 898 |
+
.bayan-il-popover-type {
|
| 899 |
+
font-size: 0.75rem !important;
|
| 900 |
+
font-weight: 700 !important;
|
| 901 |
+
margin-bottom: 0.5rem !important;
|
| 902 |
+
display: flex !important;
|
| 903 |
+
align-items: center !important;
|
| 904 |
+
gap: 0.5rem !important;
|
| 905 |
+
}
|
| 906 |
+
.bayan-il-popover-type--spelling { color: #E88A8A !important; }
|
| 907 |
+
.bayan-il-popover-type--grammar { color: #E4B35A !important; }
|
| 908 |
+
.bayan-il-popover-type--punctuation { color: #6BC98A !important; }
|
| 909 |
+
|
| 910 |
+
.bayan-il-popover-original-word {
|
| 911 |
+
font-size: 0.875rem !important;
|
| 912 |
+
color: #B4BBC6 !important;
|
| 913 |
+
margin-bottom: 0.5rem !important;
|
| 914 |
+
text-align: right !important;
|
| 915 |
+
}
|
| 916 |
+
.bayan-il-popover-label {
|
| 917 |
+
font-weight: 700 !important;
|
| 918 |
+
}
|
| 919 |
+
.bayan-il-popover-alternatives {
|
| 920 |
+
display: flex !important;
|
| 921 |
+
flex-direction: column !important;
|
| 922 |
+
gap: 6px !important;
|
| 923 |
+
margin-bottom: 0.5rem !important;
|
| 924 |
+
}
|
| 925 |
+
.bayan-il-popover-alt-btn {
|
| 926 |
+
width: 100% !important;
|
| 927 |
+
padding: 8px 12px !important;
|
| 928 |
+
border: 1px solid rgba(236, 238, 242, 0.09) !important;
|
| 929 |
+
border-radius: 0.75rem !important;
|
| 930 |
+
background: #1A1D26 !important;
|
| 931 |
+
color: #ECEEF2 !important;
|
| 932 |
+
font-weight: 600 !important;
|
| 933 |
+
cursor: pointer !important;
|
| 934 |
+
font-family: inherit !important;
|
| 935 |
+
font-size: 1rem !important;
|
| 936 |
+
text-align: right !important;
|
| 937 |
+
transition: all 0.2s ease !important;
|
| 938 |
+
min-height: 40px !important;
|
| 939 |
+
}
|
| 940 |
+
.bayan-il-popover-alt-main {
|
| 941 |
+
background: linear-gradient(135deg, #6BA3E0, #A594E8) !important;
|
| 942 |
+
color: #F4F5F7 !important;
|
| 943 |
+
border-color: transparent !important;
|
| 944 |
+
font-weight: 700 !important;
|
| 945 |
+
}
|
| 946 |
+
.bayan-il-popover-alt-main:hover {
|
| 947 |
+
opacity: 0.92 !important;
|
| 948 |
+
transform: translateX(-2px) !important;
|
| 949 |
+
}
|
| 950 |
+
.bayan-il-popover-dismiss {
|
| 951 |
+
display: block !important;
|
| 952 |
+
width: 100% !important;
|
| 953 |
+
padding: 6px 0 !important;
|
| 954 |
+
margin-top: 6px !important;
|
| 955 |
+
background: none !important;
|
| 956 |
+
border: 1px dashed rgba(236, 238, 242, 0.16) !important;
|
| 957 |
+
border-radius: 0.5rem !important;
|
| 958 |
+
color: #8A939F !important;
|
| 959 |
+
font-size: 0.75rem !important;
|
| 960 |
+
font-family: inherit !important;
|
| 961 |
+
cursor: pointer !important;
|
| 962 |
+
transition: all 0.15s ease !important;
|
| 963 |
+
}
|
| 964 |
+
.bayan-il-popover-dismiss:hover {
|
| 965 |
+
color: #E88A8A !important;
|
| 966 |
+
border-color: #E88A8A !important;
|
| 967 |
+
background: rgba(232, 138, 138, 0.08) !important;
|
| 968 |
+
}
|
| 969 |
+
.bayan-il-popover-hint {
|
| 970 |
+
font-size: 0.75rem !important;
|
| 971 |
+
color: #8A939F !important;
|
| 972 |
+
text-align: center !important;
|
| 973 |
+
margin-top: 0.5rem !important;
|
| 974 |
+
margin-bottom: 0 !important;
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
/* Modal Panel UI Sync */
|
| 978 |
+
.bayan-il-sidebar-panel {
|
| 979 |
+
display: flex !important;
|
| 980 |
+
flex-direction: column !important;
|
| 981 |
+
gap: 1rem !important;
|
| 982 |
+
max-height: calc(100vh - 140px) !important;
|
| 983 |
+
overflow-y: auto !important;
|
| 984 |
+
scrollbar-width: thin !important;
|
| 985 |
+
font-family: 'Cairo', 'Tajawal', 'Noto Sans Arabic', sans-serif !important;
|
| 986 |
+
color: #ECEEF2 !important;
|
| 987 |
+
}
|
| 988 |
+
.bayan-il-sidebar-card {
|
| 989 |
+
padding: 1.5rem !important;
|
| 990 |
+
}
|
| 991 |
+
.bayan-il-surface-card {
|
| 992 |
+
background: #1A1D26 !important;
|
| 993 |
+
border-radius: 1rem !important;
|
| 994 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28) !important;
|
| 995 |
+
border: 1px solid rgba(26, 29, 33, 0.08) !important;
|
| 996 |
+
}
|
| 997 |
+
.bayan-il-card-title {
|
| 998 |
+
font-size: 1.25rem !important;
|
| 999 |
+
font-weight: 700 !important;
|
| 1000 |
+
margin-top: 0 !important;
|
| 1001 |
+
margin-bottom: 1rem !important;
|
| 1002 |
+
text-align: right !important;
|
| 1003 |
+
color: #ECEEF2 !important;
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
/* Score Ring */
|
| 1007 |
+
.bayan-il-score-ring-wrap {
|
| 1008 |
+
position: relative !important;
|
| 1009 |
+
width: 140px !important;
|
| 1010 |
+
height: 140px !important;
|
| 1011 |
+
margin: 0 auto 1rem !important;
|
| 1012 |
+
}
|
| 1013 |
+
.bayan-il-score-svg {
|
| 1014 |
+
width: 100% !important;
|
| 1015 |
+
height: 100% !important;
|
| 1016 |
+
transform: rotate(-90deg) !important;
|
| 1017 |
+
}
|
| 1018 |
+
.bayan-il-score-circle {
|
| 1019 |
+
transition: stroke-dashoffset 0.6s ease-in-out !important;
|
| 1020 |
+
}
|
| 1021 |
+
.bayan-il-score-value {
|
| 1022 |
+
position: absolute !important;
|
| 1023 |
+
inset: 0 !important;
|
| 1024 |
+
display: flex !important;
|
| 1025 |
+
align-items: center !important;
|
| 1026 |
+
justify-content: center !important;
|
| 1027 |
+
font-size: 2.25rem !important;
|
| 1028 |
+
font-weight: 700 !important;
|
| 1029 |
+
color: #ECEEF2 !important;
|
| 1030 |
+
}
|
| 1031 |
+
.bayan-il-score-hint {
|
| 1032 |
+
text-align: center !important;
|
| 1033 |
+
font-size: 0.875rem !important;
|
| 1034 |
+
color: #B4BBC6 !important;
|
| 1035 |
+
margin: 0 !important;
|
| 1036 |
+
}
|
| 1037 |
+
|
| 1038 |
+
/* Suggestions scroll & card */
|
| 1039 |
+
.bayan-il-suggestions-scroll {
|
| 1040 |
+
max-height: 320px !important;
|
| 1041 |
+
overflow-y: auto !important;
|
| 1042 |
+
display: flex !important;
|
| 1043 |
+
flex-direction: column !important;
|
| 1044 |
+
gap: 0.5rem !important;
|
| 1045 |
+
}
|
| 1046 |
+
.bayan-il-suggestion-card {
|
| 1047 |
+
padding: 1rem !important;
|
| 1048 |
+
padding-right: calc(1rem + 4px) !important;
|
| 1049 |
+
border-radius: 0.75rem !important;
|
| 1050 |
+
border: 1px solid rgba(236, 238, 242, 0.09) !important;
|
| 1051 |
+
border-right: 4px solid rgba(236, 238, 242, 0.16) !important;
|
| 1052 |
+
background: #242833 !important;
|
| 1053 |
+
cursor: pointer !important;
|
| 1054 |
+
transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease !important;
|
| 1055 |
+
text-align: right !important;
|
| 1056 |
+
}
|
| 1057 |
+
.bayan-il-suggestion-card[data-suggestion-type="spelling"] { border-right-color: #E88A8A !important; }
|
| 1058 |
+
.bayan-il-suggestion-card[data-suggestion-type="grammar"] { border-right-color: #E4B35A !important; }
|
| 1059 |
+
.bayan-il-suggestion-card[data-suggestion-type="punctuation"] { border-right-color: #6BC98A !important; }
|
| 1060 |
+
.bayan-il-suggestion-card:hover {
|
| 1061 |
+
border-color: #6BA3E0 !important;
|
| 1062 |
+
background: rgba(107, 163, 224, 0.12) !important;
|
| 1063 |
+
transform: translateX(-2px) !important;
|
| 1064 |
+
}
|
| 1065 |
+
.bayan-il-suggestion-card-badge {
|
| 1066 |
+
display: inline-block !important;
|
| 1067 |
+
font-size: 0.75rem !important;
|
| 1068 |
+
font-weight: 700 !important;
|
| 1069 |
+
padding: 2px 8px !important;
|
| 1070 |
+
border-radius: 999px !important;
|
| 1071 |
+
margin-bottom: 0.5rem !important;
|
| 1072 |
+
}
|
| 1073 |
+
.bayan-il-suggestion-card-change {
|
| 1074 |
+
font-size: 0.875rem !important;
|
| 1075 |
+
margin-bottom: 4px !important;
|
| 1076 |
+
color: #ECEEF2 !important;
|
| 1077 |
+
}
|
| 1078 |
+
.bayan-il-suggestion-card-original {
|
| 1079 |
+
text-decoration: line-through !important;
|
| 1080 |
+
color: #8A939F !important;
|
| 1081 |
+
}
|
| 1082 |
+
.bayan-il-suggestion-card-arrow {
|
| 1083 |
+
margin: 0 0.5rem !important;
|
| 1084 |
+
color: #8A939F !important;
|
| 1085 |
+
}
|
| 1086 |
+
.bayan-il-suggestion-card-fix {
|
| 1087 |
+
color: #6BC98A !important;
|
| 1088 |
+
font-weight: 600 !important;
|
| 1089 |
+
}
|
| 1090 |
+
.bayan-il-suggestion-card-alts {
|
| 1091 |
+
display: flex !important;
|
| 1092 |
+
flex-wrap: wrap !important;
|
| 1093 |
+
gap: 6px !important;
|
| 1094 |
+
margin-top: 8px !important;
|
| 1095 |
+
}
|
| 1096 |
+
.bayan-il-suggestion-card-apply {
|
| 1097 |
+
padding: 4px 10px !important;
|
| 1098 |
+
border: none !important;
|
| 1099 |
+
border-radius: 0.5rem !important;
|
| 1100 |
+
background: #6BA3E0 !important;
|
| 1101 |
+
color: #F4F5F7 !important;
|
| 1102 |
+
font-size: 0.75rem !important;
|
| 1103 |
+
font-weight: 700 !important;
|
| 1104 |
+
cursor: pointer !important;
|
| 1105 |
+
font-family: inherit !important;
|
| 1106 |
+
min-height: 32px !important;
|
| 1107 |
+
transition: opacity 0.2s ease !important;
|
| 1108 |
+
}
|
| 1109 |
+
.bayan-il-suggestion-card-apply:hover {
|
| 1110 |
+
opacity: 0.9 !important;
|
| 1111 |
+
}
|
| 1112 |
+
.bayan-il-suggestion-card-alt-keep {
|
| 1113 |
+
background: transparent !important;
|
| 1114 |
+
border: 1px dashed rgba(236, 238, 242, 0.09) !important;
|
| 1115 |
+
color: #8A939F !important;
|
| 1116 |
+
font-size: 0.75rem !important;
|
| 1117 |
+
font-weight: 500 !important;
|
| 1118 |
+
padding: 4px 10px !important;
|
| 1119 |
+
border-radius: 0.5rem !important;
|
| 1120 |
+
cursor: pointer !important;
|
| 1121 |
+
}
|
| 1122 |
+
.bayan-il-suggestion-card-alt-keep:hover {
|
| 1123 |
+
color: #ECEEF2 !important;
|
| 1124 |
+
border-color: #B4BBC6 !important;
|
| 1125 |
+
background: #242833 !important;
|
| 1126 |
+
}
|
| 1127 |
+
.bayan-il-apply-all-btn {
|
| 1128 |
+
width: 100% !important;
|
| 1129 |
+
padding: 8px 16px !important;
|
| 1130 |
+
background: linear-gradient(135deg, #6BA3E0, #A594E8) !important;
|
| 1131 |
+
color: #F4F5F7 !important;
|
| 1132 |
+
font-size: 1rem !important;
|
| 1133 |
+
font-weight: 700 !important;
|
| 1134 |
+
border: none !important;
|
| 1135 |
+
border-radius: 0.75rem !important;
|
| 1136 |
+
cursor: pointer !important;
|
| 1137 |
+
margin-top: 1rem !important;
|
| 1138 |
+
transition: opacity 0.2s ease !important;
|
| 1139 |
+
}
|
| 1140 |
+
.bayan-il-apply-all-btn:hover {
|
| 1141 |
+
opacity: 0.9 !important;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
/* Modal header wrapper */
|
| 1145 |
+
.bayan-il-modal-top-bar {
|
| 1146 |
+
display: flex !important;
|
| 1147 |
+
align-items: center !important;
|
| 1148 |
+
justify-content: space-between !important;
|
| 1149 |
+
padding: 0 0.5rem !important;
|
| 1150 |
+
}
|
| 1151 |
+
.bayan-il-modal-brand {
|
| 1152 |
+
display: flex !important;
|
| 1153 |
+
align-items: center !important;
|
| 1154 |
+
gap: 0.5rem !important;
|
| 1155 |
+
}
|
| 1156 |
+
.bayan-il-modal-title {
|
| 1157 |
+
font-size: 1.25rem !important;
|
| 1158 |
+
font-weight: 700 !important;
|
| 1159 |
+
background: linear-gradient(135deg, #6BA3E0, #A594E8) !important;
|
| 1160 |
+
-webkit-background-clip: text !important;
|
| 1161 |
+
-webkit-text-fill-color: transparent !important;
|
| 1162 |
+
}
|
| 1163 |
+
.bayan-il-modal-close {
|
| 1164 |
+
background: none !important;
|
| 1165 |
+
border: none !important;
|
| 1166 |
+
color: #B4BBC6 !important;
|
| 1167 |
+
font-size: 1.25rem !important;
|
| 1168 |
+
cursor: pointer !important;
|
| 1169 |
+
}
|
| 1170 |
+
.bayan-il-modal-close:hover {
|
| 1171 |
+
color: #E88A8A !important;
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
|
| 1175 |
+
|
| 1176 |
+
|
| 1177 |
+
.bayan-il-header-divider {
|
| 1178 |
+
width: 1px !important;
|
| 1179 |
+
height: 18px !important;
|
| 1180 |
+
background: rgba(236, 238, 242, 0.2) !important;
|
| 1181 |
+
margin: 0 10px !important;
|
| 1182 |
+
}
|
| 1183 |
+
|
| 1184 |
+
.bayan-il-modal-top-bar {
|
| 1185 |
+
cursor: grab !important;
|
| 1186 |
+
}
|
| 1187 |
+
|
| 1188 |
+
.bayan-il-modal-top-bar:active {
|
| 1189 |
+
cursor: grabbing !important;
|
| 1190 |
}
|
| 1191 |
|
extension/content-inline.js
CHANGED
|
@@ -66,6 +66,7 @@
|
|
| 66 |
let overlayContainer = null;
|
| 67 |
let badgeCount = null;
|
| 68 |
let observer = null;
|
|
|
|
| 69 |
|
| 70 |
// ── Ghost-text autocomplete state ──
|
| 71 |
let ghostEl = null; // the ghost overlay element
|
|
@@ -182,10 +183,10 @@
|
|
| 182 |
const tag = activeField.tagName.toLowerCase();
|
| 183 |
if (tag !== 'textarea' && tag !== 'input') return false;
|
| 184 |
const val = activeField.value || '';
|
| 185 |
-
// Only when the caret is collapsed at the very end of the text.
|
| 186 |
if (activeField.selectionStart !== val.length) return false;
|
| 187 |
if (activeField.selectionEnd !== val.length) return false;
|
| 188 |
if (val.trim().length < AC_MIN_CONTEXT) return false;
|
|
|
|
| 189 |
return /[-ۿ]/.test(val);
|
| 190 |
}
|
| 191 |
|
|
@@ -237,11 +238,12 @@
|
|
| 237 |
try {
|
| 238 |
const rect = activeField.getBoundingClientRect();
|
| 239 |
const cs = window.getComputedStyle(activeField);
|
|
|
|
| 240 |
|
| 241 |
ghostEl = document.createElement('div');
|
| 242 |
ghostEl.className = 'bayan-il-ghost';
|
| 243 |
ghostEl.style.cssText = `position:fixed;top:${rect.top}px;`
|
| 244 |
-
+ `left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;`
|
| 245 |
+ `font-family:${cs.fontFamily};font-size:${cs.fontSize};line-height:${cs.lineHeight};`
|
| 246 |
+ `padding:${cs.padding};border:${cs.border};border-color:transparent;`
|
| 247 |
+ `direction:${cs.direction};text-align:${cs.textAlign};`
|
|
@@ -336,11 +338,12 @@
|
|
| 336 |
try {
|
| 337 |
const rect = field.getBoundingClientRect();
|
| 338 |
const cs = window.getComputedStyle(field);
|
|
|
|
| 339 |
|
| 340 |
overlayContainer = document.createElement('div');
|
| 341 |
overlayContainer.className = 'bayan-il-overlay';
|
| 342 |
overlayContainer.style.cssText = `position:fixed;top:${rect.top}px;`
|
| 343 |
-
+ `left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;`
|
| 344 |
+ `font-family:${cs.fontFamily};font-size:${cs.fontSize};line-height:${cs.lineHeight};`
|
| 345 |
+ `padding:${cs.padding};border:${cs.border};border-color:transparent;`
|
| 346 |
+ `direction:${cs.direction};text-align:${cs.textAlign};`
|
|
@@ -394,6 +397,49 @@
|
|
| 394 |
syncGhostScroll();
|
| 395 |
}
|
| 396 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
function clearHighlights() {
|
| 398 |
if (overlayContainer) { overlayContainer.remove(); overlayContainer = null; }
|
| 399 |
hideTooltip();
|
|
@@ -420,23 +466,28 @@
|
|
| 420 |
|
| 421 |
tooltip = document.createElement('div');
|
| 422 |
tooltip.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 423 |
-
tooltip.className = 'bayan-il-
|
| 424 |
tooltip.dir = 'rtl';
|
| 425 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
safeHTML(tooltip, `
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
<
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
<
|
| 434 |
-
<
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
<
|
| 439 |
-
|
|
|
|
| 440 |
`);
|
| 441 |
|
| 442 |
document.body.appendChild(tooltip);
|
|
@@ -468,13 +519,11 @@
|
|
| 468 |
hideTooltip();
|
| 469 |
});
|
| 470 |
|
| 471 |
-
tooltip.querySelector('
|
| 472 |
dismissSuggestion(suggestion);
|
| 473 |
hideTooltip();
|
| 474 |
});
|
| 475 |
|
| 476 |
-
tooltip.querySelector('.bayan-il-tooltip-close').addEventListener('click', () => hideTooltip());
|
| 477 |
-
|
| 478 |
setTimeout(() => document.addEventListener('click', outsideClick, { once: true }), 100);
|
| 479 |
}
|
| 480 |
|
|
@@ -547,6 +596,7 @@
|
|
| 547 |
let fabDragOffsetX = 0;
|
| 548 |
let fabDragOffsetY = 0;
|
| 549 |
let fabCustomPos = null; // {x, y} when user has dragged to a custom position
|
|
|
|
| 550 |
|
| 551 |
function loadFabPosition() {
|
| 552 |
try {
|
|
@@ -569,18 +619,9 @@
|
|
| 569 |
floatingBtn = document.createElement('div');
|
| 570 |
floatingBtn.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 571 |
floatingBtn.className = 'bayan-il-fab';
|
|
|
|
| 572 |
safeHTML(floatingBtn, `
|
| 573 |
-
<
|
| 574 |
-
<circle cx="50" cy="50" r="46" fill="url(#blGrad)" />
|
| 575 |
-
<path d="M30 55 Q35 35, 50 30 Q65 35, 70 55 Q65 65, 50 70 Q35 65, 30 55Z" fill="rgba(255,255,255,0.9)" />
|
| 576 |
-
<circle cx="50" cy="42" r="4" fill="url(#blGrad)" />
|
| 577 |
-
<defs>
|
| 578 |
-
<linearGradient id="blGrad" x1="0" y1="0" x2="100" y2="100">
|
| 579 |
-
<stop offset="0%" stop-color="#6366f1"/>
|
| 580 |
-
<stop offset="100%" stop-color="#8b5cf6"/>
|
| 581 |
-
</linearGradient>
|
| 582 |
-
</defs>
|
| 583 |
-
</svg>
|
| 584 |
<span class="bayan-il-badge">0</span>
|
| 585 |
`);
|
| 586 |
floatingBtn.title = 'Bayan — بيان';
|
|
@@ -712,6 +753,8 @@
|
|
| 712 |
field.addEventListener('select', onFieldSelect);
|
| 713 |
// FIX-20: Removed redundant 'keyup' listener (input event already fires on every change)
|
| 714 |
|
|
|
|
|
|
|
| 715 |
if (BayanController.hasArabic(getFieldText(field))) {
|
| 716 |
onFieldInput();
|
| 717 |
}
|
|
@@ -727,6 +770,7 @@
|
|
| 727 |
// FIX-20: keyup listener no longer attached
|
| 728 |
activeField.removeEventListener('scroll', syncOverlay);
|
| 729 |
}
|
|
|
|
| 730 |
clearHighlights();
|
| 731 |
clearGhost();
|
| 732 |
BayanController.cancelAll();
|
|
@@ -995,31 +1039,21 @@
|
|
| 995 |
if (floatingBtn?.contains(a)) return;
|
| 996 |
if (overlayContainer?.contains(a)) return;
|
| 997 |
if (modalPanel?.contains(a)) return;
|
| 998 |
-
if (
|
| 999 |
if (document.querySelector('.bayan-il-tooltip')) return;
|
| 1000 |
-
|
|
|
|
|
|
|
|
|
|
| 1001 |
}, 300);
|
| 1002 |
}, true);
|
| 1003 |
|
| 1004 |
window.addEventListener('scroll', () => {
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
const rect = activeField.getBoundingClientRect();
|
| 1008 |
-
overlayContainer.style.top = `${rect.top}px`;
|
| 1009 |
-
overlayContainer.style.left = `${rect.left}px`;
|
| 1010 |
-
}
|
| 1011 |
-
if (ghostEl && activeField) {
|
| 1012 |
-
const rect = activeField.getBoundingClientRect();
|
| 1013 |
-
ghostEl.style.top = `${rect.top}px`;
|
| 1014 |
-
ghostEl.style.left = `${rect.left}px`;
|
| 1015 |
-
}
|
| 1016 |
-
}, { passive: true });
|
| 1017 |
|
| 1018 |
window.addEventListener('resize', () => {
|
| 1019 |
-
|
| 1020 |
-
// B4: a viewport resize can change the field's width/position, leaving the
|
| 1021 |
-
// overlay marks misaligned. Re-render the overlay (not just the FAB) so
|
| 1022 |
-
// highlights track the field. Guarded by suggestions to avoid needless work.
|
| 1023 |
if (overlayContainer && activeField && suggestions.length > 0) {
|
| 1024 |
renderOverlay(activeField, lastAnalyzedText, suggestions);
|
| 1025 |
}
|
|
@@ -1059,7 +1093,6 @@
|
|
| 1059 |
// Modal Dialog — Full Analysis Panel
|
| 1060 |
// ══════════════════════════════════════════════════════════
|
| 1061 |
|
| 1062 |
-
let modalBackdrop = null;
|
| 1063 |
let modalPanel = null;
|
| 1064 |
|
| 1065 |
const TYPE_LABELS = { spelling: 'إملائي', grammar: 'نحوي', punctuation: 'ترقيم' };
|
|
@@ -1092,11 +1125,7 @@
|
|
| 1092 |
}
|
| 1093 |
|
| 1094 |
function createModal() {
|
| 1095 |
-
if (
|
| 1096 |
-
|
| 1097 |
-
modalBackdrop = document.createElement('div');
|
| 1098 |
-
modalBackdrop.className = 'bayan-il-modal-backdrop';
|
| 1099 |
-
modalBackdrop.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1100 |
|
| 1101 |
modalPanel = document.createElement('div');
|
| 1102 |
modalPanel.className = 'bayan-il-modal-panel';
|
|
@@ -1104,68 +1133,92 @@
|
|
| 1104 |
modalPanel.dir = 'rtl';
|
| 1105 |
|
| 1106 |
safeHTML(modalPanel, `
|
| 1107 |
-
<div class="bayan-il-modal-
|
| 1108 |
<div class="bayan-il-modal-brand">
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
<circle cx="50" cy="42" r="4" fill="url(#bmGrad)" />
|
| 1113 |
-
<defs>
|
| 1114 |
-
<linearGradient id="bmGrad" x1="0" y1="0" x2="100" y2="100">
|
| 1115 |
-
<stop offset="0%" stop-color="#6366f1"/>
|
| 1116 |
-
<stop offset="100%" stop-color="#8b5cf6"/>
|
| 1117 |
-
</linearGradient>
|
| 1118 |
-
</defs>
|
| 1119 |
-
</svg>
|
| 1120 |
-
<span class="bayan-il-modal-title">بيان</span>
|
| 1121 |
</div>
|
| 1122 |
<button class="bayan-il-modal-close" title="إغلاق">✕</button>
|
| 1123 |
</div>
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
|
|
|
| 1132 |
</svg>
|
| 1133 |
-
<
|
| 1134 |
</div>
|
| 1135 |
-
<
|
| 1136 |
-
|
| 1137 |
-
<
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
<span class="bayan-il-modal-count bayan-il-modal-count--punctuation">ترقيم: <strong id="bayan-modal-count-punctuation">٠</strong></span>
|
| 1141 |
-
</div>
|
| 1142 |
</div>
|
| 1143 |
</div>
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1147 |
</div>
|
| 1148 |
-
<div class="bayan-il-modal-cards" id="bayan-modal-cards"></div>
|
| 1149 |
-
<button class="bayan-il-modal-apply-all" id="bayan-modal-apply-all" style="display:none;">
|
| 1150 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
| 1151 |
-
تطبيق الكل
|
| 1152 |
-
</button>
|
| 1153 |
</div>
|
| 1154 |
`);
|
| 1155 |
|
| 1156 |
-
document.body.appendChild(modalBackdrop);
|
| 1157 |
document.body.appendChild(modalPanel);
|
| 1158 |
|
| 1159 |
modalPanel.querySelector('.bayan-il-modal-close').addEventListener('click', hideModal);
|
| 1160 |
-
modalBackdrop.addEventListener('click', hideModal);
|
| 1161 |
|
| 1162 |
modalPanel.querySelector('#bayan-modal-apply-all').addEventListener('click', applyAllFixes);
|
| 1163 |
|
| 1164 |
document.addEventListener('keydown', onModalKeydown);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1165 |
}
|
| 1166 |
|
| 1167 |
function onModalKeydown(e) {
|
| 1168 |
-
if (e.key === 'Escape' &&
|
| 1169 |
hideModal();
|
| 1170 |
}
|
| 1171 |
}
|
|
@@ -1173,14 +1226,43 @@
|
|
| 1173 |
function showModal() {
|
| 1174 |
createModal();
|
| 1175 |
|
| 1176 |
-
modalBackdrop.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1177 |
modalPanel.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1178 |
|
| 1179 |
-
updateModalThemeColors();
|
| 1180 |
updateModalScore();
|
| 1181 |
renderModalSuggestions();
|
| 1182 |
|
| 1183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1184 |
requestAnimationFrame(() => {
|
| 1185 |
modalPanel.classList.add('bayan-il-modal-panel--visible');
|
| 1186 |
});
|
|
@@ -1188,16 +1270,9 @@
|
|
| 1188 |
|
| 1189 |
function hideModal() {
|
| 1190 |
if (modalPanel) modalPanel.classList.remove('bayan-il-modal-panel--visible');
|
| 1191 |
-
if (modalBackdrop) modalBackdrop.classList.remove('bayan-il-modal-backdrop--visible');
|
| 1192 |
}
|
| 1193 |
|
| 1194 |
-
|
| 1195 |
-
if (!modalPanel) return;
|
| 1196 |
-
const scoreTrack = modalPanel.querySelector('#bayan-modal-score-circle')?.previousElementSibling;
|
| 1197 |
-
if (scoreTrack) {
|
| 1198 |
-
scoreTrack.setAttribute('stroke', currentBayanTheme === 'light' ? '#e5e7eb' : '#2d2d3d');
|
| 1199 |
-
}
|
| 1200 |
-
}
|
| 1201 |
|
| 1202 |
function updateModalScore() {
|
| 1203 |
if (!modalPanel) return;
|
|
@@ -1209,7 +1284,7 @@
|
|
| 1209 |
|
| 1210 |
const score = calculateWritingScore(counts.spelling, counts.grammar, counts.punctuation);
|
| 1211 |
const color = getScoreColor(score);
|
| 1212 |
-
const offset =
|
| 1213 |
|
| 1214 |
const circle = modalPanel.querySelector('#bayan-modal-score-circle');
|
| 1215 |
const valueEl = modalPanel.querySelector('#bayan-modal-score-value');
|
|
@@ -1221,7 +1296,10 @@
|
|
| 1221 |
circle.setAttribute('stroke-dashoffset', String(offset));
|
| 1222 |
});
|
| 1223 |
}
|
| 1224 |
-
if (valueEl)
|
|
|
|
|
|
|
|
|
|
| 1225 |
if (hintEl) hintEl.textContent = getScoreHint(score);
|
| 1226 |
|
| 1227 |
const toArabicNum = (n) => String(n).replace(/\d/g, (d) => '٠١٢٣٤٥٦٧٨٩'[d]);
|
|
@@ -1256,7 +1334,7 @@
|
|
| 1256 |
}
|
| 1257 |
|
| 1258 |
const toArabicNum = (n) => String(n).replace(/\d/g, (d) => '٠١٢٣٤٥٦٧٨٩'[d]);
|
| 1259 |
-
if (countEl) countEl.textContent =
|
| 1260 |
if (applyAllBtn) applyAllBtn.style.display = suggestions.length >= 2 ? 'flex' : 'none';
|
| 1261 |
|
| 1262 |
let html = '';
|
|
@@ -1264,7 +1342,7 @@
|
|
| 1264 |
const alts = resolveAlternatives(s);
|
| 1265 |
const typeLabel = TYPE_LABELS[s.type] || s.type;
|
| 1266 |
|
| 1267 |
-
html += `<div class="bayan-il-modal-card bayan-il-modal-card--${s.type}" data-modal-idx="${idx}">`;
|
| 1268 |
html += `<span class="bayan-il-modal-card-badge bayan-il-modal-badge--${s.type}">${typeLabel}</span>`;
|
| 1269 |
html += `<div class="bayan-il-modal-card-change">`;
|
| 1270 |
html += `<span class="bayan-il-modal-card-original">${esc(s.original)}</span>`;
|
|
@@ -1276,10 +1354,8 @@
|
|
| 1276 |
alts.forEach((alt, ai) => {
|
| 1277 |
const isMain = alt === s.correction && ai === 0;
|
| 1278 |
const isKeep = alt === s.original;
|
| 1279 |
-
let cls = 'bayan-il-modal-alt-chip';
|
| 1280 |
-
if (isMain) cls += ' bayan-il-modal-alt-chip--main';
|
| 1281 |
-
else if (isKeep) cls += ' bayan-il-modal-alt-chip--keep';
|
| 1282 |
const label = isKeep ? 'إبقاء الأصل' : esc(alt);
|
|
|
|
| 1283 |
html += `<button class="${cls}" data-modal-alt="${esc(alt)}" data-modal-sidx="${idx}" data-modal-keep="${isKeep ? '1' : ''}">${label}</button>`;
|
| 1284 |
});
|
| 1285 |
|
|
@@ -1334,8 +1410,7 @@
|
|
| 1334 |
currentBayanTheme = res.theme || 'dark';
|
| 1335 |
if (typeof tooltip !== 'undefined' && tooltip) tooltip.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1336 |
if (typeof floatingBtn !== 'undefined' && floatingBtn) floatingBtn.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1337 |
-
if (
|
| 1338 |
-
if (modalPanel) { modalPanel.setAttribute('data-bayan-theme', currentBayanTheme); updateModalThemeColors(); }
|
| 1339 |
});
|
| 1340 |
|
| 1341 |
chrome.storage.onChanged.addListener((changes, namespace) => {
|
|
@@ -1343,8 +1418,7 @@
|
|
| 1343 |
currentBayanTheme = changes.theme.newValue;
|
| 1344 |
if (typeof tooltip !== 'undefined' && tooltip) tooltip.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1345 |
if (typeof floatingBtn !== 'undefined' && floatingBtn) floatingBtn.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1346 |
-
if (
|
| 1347 |
-
if (modalPanel) { modalPanel.setAttribute('data-bayan-theme', currentBayanTheme); updateModalThemeColors(); }
|
| 1348 |
}
|
| 1349 |
});
|
| 1350 |
|
|
|
|
| 66 |
let overlayContainer = null;
|
| 67 |
let badgeCount = null;
|
| 68 |
let observer = null;
|
| 69 |
+
let ancestorScrollCleanups = [];
|
| 70 |
|
| 71 |
// ── Ghost-text autocomplete state ──
|
| 72 |
let ghostEl = null; // the ghost overlay element
|
|
|
|
| 183 |
const tag = activeField.tagName.toLowerCase();
|
| 184 |
if (tag !== 'textarea' && tag !== 'input') return false;
|
| 185 |
const val = activeField.value || '';
|
|
|
|
| 186 |
if (activeField.selectionStart !== val.length) return false;
|
| 187 |
if (activeField.selectionEnd !== val.length) return false;
|
| 188 |
if (val.trim().length < AC_MIN_CONTEXT) return false;
|
| 189 |
+
if (!/[\s ]$/.test(val)) return false;
|
| 190 |
return /[-ۿ]/.test(val);
|
| 191 |
}
|
| 192 |
|
|
|
|
| 238 |
try {
|
| 239 |
const rect = activeField.getBoundingClientRect();
|
| 240 |
const cs = window.getComputedStyle(activeField);
|
| 241 |
+
const sbW = activeField.offsetWidth - activeField.clientWidth;
|
| 242 |
|
| 243 |
ghostEl = document.createElement('div');
|
| 244 |
ghostEl.className = 'bayan-il-ghost';
|
| 245 |
ghostEl.style.cssText = `position:fixed;top:${rect.top}px;`
|
| 246 |
+
+ `left:${rect.left}px;width:${rect.width - sbW}px;height:${rect.height}px;`
|
| 247 |
+ `font-family:${cs.fontFamily};font-size:${cs.fontSize};line-height:${cs.lineHeight};`
|
| 248 |
+ `padding:${cs.padding};border:${cs.border};border-color:transparent;`
|
| 249 |
+ `direction:${cs.direction};text-align:${cs.textAlign};`
|
|
|
|
| 338 |
try {
|
| 339 |
const rect = field.getBoundingClientRect();
|
| 340 |
const cs = window.getComputedStyle(field);
|
| 341 |
+
const sbW = field.offsetWidth - field.clientWidth;
|
| 342 |
|
| 343 |
overlayContainer = document.createElement('div');
|
| 344 |
overlayContainer.className = 'bayan-il-overlay';
|
| 345 |
overlayContainer.style.cssText = `position:fixed;top:${rect.top}px;`
|
| 346 |
+
+ `left:${rect.left}px;width:${rect.width - sbW}px;height:${rect.height}px;`
|
| 347 |
+ `font-family:${cs.fontFamily};font-size:${cs.fontSize};line-height:${cs.lineHeight};`
|
| 348 |
+ `padding:${cs.padding};border:${cs.border};border-color:transparent;`
|
| 349 |
+ `direction:${cs.direction};text-align:${cs.textAlign};`
|
|
|
|
| 397 |
syncGhostScroll();
|
| 398 |
}
|
| 399 |
|
| 400 |
+
function repositionOverlay() {
|
| 401 |
+
if (!activeField) return;
|
| 402 |
+
const rect = activeField.getBoundingClientRect();
|
| 403 |
+
const sbW = activeField.offsetWidth - activeField.clientWidth;
|
| 404 |
+
if (overlayContainer) {
|
| 405 |
+
overlayContainer.style.top = `${rect.top}px`;
|
| 406 |
+
overlayContainer.style.left = `${rect.left}px`;
|
| 407 |
+
overlayContainer.style.width = `${rect.width - sbW}px`;
|
| 408 |
+
overlayContainer.style.height = `${rect.height}px`;
|
| 409 |
+
overlayContainer.scrollTop = activeField.scrollTop;
|
| 410 |
+
overlayContainer.scrollLeft = activeField.scrollLeft;
|
| 411 |
+
}
|
| 412 |
+
if (ghostEl) {
|
| 413 |
+
ghostEl.style.top = `${rect.top}px`;
|
| 414 |
+
ghostEl.style.left = `${rect.left}px`;
|
| 415 |
+
ghostEl.style.width = `${rect.width - sbW}px`;
|
| 416 |
+
ghostEl.style.height = `${rect.height}px`;
|
| 417 |
+
ghostEl.scrollTop = activeField.scrollTop;
|
| 418 |
+
ghostEl.scrollLeft = activeField.scrollLeft;
|
| 419 |
+
}
|
| 420 |
+
if (floatingBtn) positionFab(activeField);
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
function watchAncestorScroll(field) {
|
| 424 |
+
unwatchAncestorScroll();
|
| 425 |
+
let el = field.parentElement;
|
| 426 |
+
while (el && el !== document.body) {
|
| 427 |
+
const ov = window.getComputedStyle(el).overflowY;
|
| 428 |
+
if (ov === 'auto' || ov === 'scroll' || ov === 'overlay') {
|
| 429 |
+
const handler = () => repositionOverlay();
|
| 430 |
+
el.addEventListener('scroll', handler, { passive: true });
|
| 431 |
+
const ref = el;
|
| 432 |
+
ancestorScrollCleanups.push(() => ref.removeEventListener('scroll', handler));
|
| 433 |
+
}
|
| 434 |
+
el = el.parentElement;
|
| 435 |
+
}
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
function unwatchAncestorScroll() {
|
| 439 |
+
ancestorScrollCleanups.forEach((fn) => fn());
|
| 440 |
+
ancestorScrollCleanups = [];
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
function clearHighlights() {
|
| 444 |
if (overlayContainer) { overlayContainer.remove(); overlayContainer = null; }
|
| 445 |
hideTooltip();
|
|
|
|
| 466 |
|
| 467 |
tooltip = document.createElement('div');
|
| 468 |
tooltip.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 469 |
+
tooltip.className = 'bayan-il-suggestion-popover bayan-il-show';
|
| 470 |
tooltip.dir = 'rtl';
|
| 471 |
|
| 472 |
+
const typeLabel = typeLabels[type] || type;
|
| 473 |
+
const typeClass = type === 'spelling' ? 'bayan-il-popover-type--spelling' : type === 'grammar' ? 'bayan-il-popover-type--grammar' : 'bayan-il-popover-type--punctuation';
|
| 474 |
+
const icon = type === 'spelling' ? '✕' : type === 'grammar' ? '!' : '✓';
|
| 475 |
+
|
| 476 |
safeHTML(tooltip, `
|
| 477 |
+
<div class="bayan-il-popover-type ${typeClass}">
|
| 478 |
+
<span class="bayan-il-popover-type-icon">${icon}</span> ${typeLabel}
|
| 479 |
+
</div>
|
| 480 |
+
<div class="bayan-il-popover-original-word">
|
| 481 |
+
<span class="bayan-il-popover-label">الكلمة:</span>
|
| 482 |
+
<span class="bayan-il-tooltip-original">${esc(original)}</span>
|
| 483 |
+
</div>
|
| 484 |
+
<div class="bayan-il-popover-alternatives">
|
| 485 |
+
<button class="bayan-il-popover-alt-btn bayan-il-popover-alt-main" data-action="apply">
|
| 486 |
+
${correction ? esc(correction) : '<s style="opacity:0.5">حذف</s>'}
|
| 487 |
+
</button>
|
| 488 |
+
</div>
|
| 489 |
+
<button type="button" class="bayan-il-popover-dismiss" data-action="ignore" title="تجاهل هذا الاقتراح">تجاهل</button>
|
| 490 |
+
<p class="bayan-il-popover-hint">اختر التصحيح المناسب</p>
|
| 491 |
`);
|
| 492 |
|
| 493 |
document.body.appendChild(tooltip);
|
|
|
|
| 519 |
hideTooltip();
|
| 520 |
});
|
| 521 |
|
| 522 |
+
tooltip.querySelector('.bayan-il-popover-dismiss').addEventListener('click', () => {
|
| 523 |
dismissSuggestion(suggestion);
|
| 524 |
hideTooltip();
|
| 525 |
});
|
| 526 |
|
|
|
|
|
|
|
| 527 |
setTimeout(() => document.addEventListener('click', outsideClick, { once: true }), 100);
|
| 528 |
}
|
| 529 |
|
|
|
|
| 596 |
let fabDragOffsetX = 0;
|
| 597 |
let fabDragOffsetY = 0;
|
| 598 |
let fabCustomPos = null; // {x, y} when user has dragged to a custom position
|
| 599 |
+
let modalCustomPos = null; // {x, y} when user drags the modal
|
| 600 |
|
| 601 |
function loadFabPosition() {
|
| 602 |
try {
|
|
|
|
| 619 |
floatingBtn = document.createElement('div');
|
| 620 |
floatingBtn.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 621 |
floatingBtn.className = 'bayan-il-fab';
|
| 622 |
+
const fabLogoUrl = chrome.runtime.getURL('assets/icons/fab_logo.png');
|
| 623 |
safeHTML(floatingBtn, `
|
| 624 |
+
<img src="${fabLogoUrl}" alt="بيان" draggable="false" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block; pointer-events: none;" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
<span class="bayan-il-badge">0</span>
|
| 626 |
`);
|
| 627 |
floatingBtn.title = 'Bayan — بيان';
|
|
|
|
| 753 |
field.addEventListener('select', onFieldSelect);
|
| 754 |
// FIX-20: Removed redundant 'keyup' listener (input event already fires on every change)
|
| 755 |
|
| 756 |
+
watchAncestorScroll(field);
|
| 757 |
+
|
| 758 |
if (BayanController.hasArabic(getFieldText(field))) {
|
| 759 |
onFieldInput();
|
| 760 |
}
|
|
|
|
| 770 |
// FIX-20: keyup listener no longer attached
|
| 771 |
activeField.removeEventListener('scroll', syncOverlay);
|
| 772 |
}
|
| 773 |
+
unwatchAncestorScroll();
|
| 774 |
clearHighlights();
|
| 775 |
clearGhost();
|
| 776 |
BayanController.cancelAll();
|
|
|
|
| 1039 |
if (floatingBtn?.contains(a)) return;
|
| 1040 |
if (overlayContainer?.contains(a)) return;
|
| 1041 |
if (modalPanel?.contains(a)) return;
|
| 1042 |
+
if (modalPanel?.contains(a)) return;
|
| 1043 |
if (document.querySelector('.bayan-il-tooltip')) return;
|
| 1044 |
+
if (document.querySelector('.bayan-il-suggestion-popover')) return;
|
| 1045 |
+
if (isEditableField(a)) return;
|
| 1046 |
+
BayanController.cancelAll();
|
| 1047 |
+
clearGhost();
|
| 1048 |
}, 300);
|
| 1049 |
}, true);
|
| 1050 |
|
| 1051 |
window.addEventListener('scroll', () => {
|
| 1052 |
+
repositionOverlay();
|
| 1053 |
+
}, true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
|
| 1055 |
window.addEventListener('resize', () => {
|
| 1056 |
+
repositionOverlay();
|
|
|
|
|
|
|
|
|
|
| 1057 |
if (overlayContainer && activeField && suggestions.length > 0) {
|
| 1058 |
renderOverlay(activeField, lastAnalyzedText, suggestions);
|
| 1059 |
}
|
|
|
|
| 1093 |
// Modal Dialog — Full Analysis Panel
|
| 1094 |
// ══════════════════════════════════════════════════════════
|
| 1095 |
|
|
|
|
| 1096 |
let modalPanel = null;
|
| 1097 |
|
| 1098 |
const TYPE_LABELS = { spelling: 'إملائي', grammar: 'نحوي', punctuation: 'ترقيم' };
|
|
|
|
| 1125 |
}
|
| 1126 |
|
| 1127 |
function createModal() {
|
| 1128 |
+
if (modalPanel) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1129 |
|
| 1130 |
modalPanel = document.createElement('div');
|
| 1131 |
modalPanel.className = 'bayan-il-modal-panel';
|
|
|
|
| 1133 |
modalPanel.dir = 'rtl';
|
| 1134 |
|
| 1135 |
safeHTML(modalPanel, `
|
| 1136 |
+
<div class="bayan-il-modal-top-bar" id="bayan-modal-drag-handle">
|
| 1137 |
<div class="bayan-il-modal-brand">
|
| 1138 |
+
<img src="${chrome.runtime.getURL('assets/icons/icon128.png')}" alt="بيان" style="width: 24px; height: 24px; object-fit: contain;" draggable="false" />
|
| 1139 |
+
<div class="bayan-il-header-divider"></div>
|
| 1140 |
+
<span class="bayan-il-modal-title">بيان</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1141 |
</div>
|
| 1142 |
<button class="bayan-il-modal-close" title="إغلاق">✕</button>
|
| 1143 |
</div>
|
| 1144 |
+
|
| 1145 |
+
<div class="bayan-il-modal-body-scroll">
|
| 1146 |
+
<div class="bayan-il-modal-score-card">
|
| 1147 |
+
<h3 class="bayan-il-modal-section-title">تقييم الكتابة</h3>
|
| 1148 |
+
<div class="bayan-il-modal-score-ring" role="img" aria-label="تقييم الكتابة">
|
| 1149 |
+
<svg viewBox="0 0 160 160" aria-hidden="true">
|
| 1150 |
+
<circle cx="80" cy="80" r="70" fill="none" stroke="rgba(236, 238, 242, 0.09)" stroke-width="10"/>
|
| 1151 |
+
<circle id="bayan-modal-score-circle" cx="80" cy="80" r="70" fill="none" stroke="url(#bayanModalScoreGradient)" stroke-width="10" stroke-linecap="round" stroke-dasharray="439.8" stroke-dashoffset="439.8" class="bayan-il-score-circle"/>
|
| 1152 |
+
<defs><linearGradient id="bayanModalScoreGradient" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#6BA3E0"/><stop offset="100%" stop-color="#A594E8"/></linearGradient></defs>
|
| 1153 |
</svg>
|
| 1154 |
+
<div class="bayan-il-modal-score-value"><span id="bayan-modal-score-value">--</span></div>
|
| 1155 |
</div>
|
| 1156 |
+
<p id="bayan-modal-score-hint" class="bayan-il-modal-score-hint">ابدأ الكتابة لرؤية تقييمك</p>
|
| 1157 |
+
<div class="bayan-il-modal-counts-row">
|
| 1158 |
+
<span class="bayan-il-modal-count bayan-il-modal-count--spelling"><strong id="bayan-modal-count-spelling">٠</strong> إملائي</span>
|
| 1159 |
+
<span class="bayan-il-modal-count bayan-il-modal-count--grammar"><strong id="bayan-modal-count-grammar">٠</strong> نحوي</span>
|
| 1160 |
+
<span class="bayan-il-modal-count bayan-il-modal-count--punctuation"><strong id="bayan-modal-count-punctuation">٠</strong> ترقيم</span>
|
|
|
|
|
|
|
| 1161 |
</div>
|
| 1162 |
</div>
|
| 1163 |
+
|
| 1164 |
+
<div class="bayan-il-modal-sugg-card">
|
| 1165 |
+
<div class="bayan-il-modal-sugg-header">
|
| 1166 |
+
<h3 class="bayan-il-modal-section-title">الاقتراحات (<span id="bayan-modal-sugg-count">٠</span>)</h3>
|
| 1167 |
+
<button id="bayan-modal-apply-all" class="bayan-il-modal-apply-all" style="display:none;" type="button">تطبيق الكل</button>
|
| 1168 |
+
</div>
|
| 1169 |
+
<div id="bayan-modal-cards" class="bayan-il-modal-cards" role="list" aria-live="polite" aria-label="اقتراحات التصحيح"></div>
|
| 1170 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1171 |
</div>
|
| 1172 |
`);
|
| 1173 |
|
|
|
|
| 1174 |
document.body.appendChild(modalPanel);
|
| 1175 |
|
| 1176 |
modalPanel.querySelector('.bayan-il-modal-close').addEventListener('click', hideModal);
|
|
|
|
| 1177 |
|
| 1178 |
modalPanel.querySelector('#bayan-modal-apply-all').addEventListener('click', applyAllFixes);
|
| 1179 |
|
| 1180 |
document.addEventListener('keydown', onModalKeydown);
|
| 1181 |
+
|
| 1182 |
+
let modalDragging = false;
|
| 1183 |
+
let modalDragStartX = 0;
|
| 1184 |
+
let modalDragStartY = 0;
|
| 1185 |
+
let modalDragOffsetX = 0;
|
| 1186 |
+
let modalDragOffsetY = 0;
|
| 1187 |
+
|
| 1188 |
+
const dragHandle = modalPanel.querySelector('#bayan-modal-drag-handle');
|
| 1189 |
+
if (dragHandle) {
|
| 1190 |
+
dragHandle.addEventListener('mousedown', (e) => {
|
| 1191 |
+
if (e.target.closest('.bayan-il-modal-close')) return;
|
| 1192 |
+
modalDragging = true;
|
| 1193 |
+
modalDragStartX = e.clientX;
|
| 1194 |
+
modalDragStartY = e.clientY;
|
| 1195 |
+
modalDragOffsetX = parseInt(modalPanel.style.left || 0, 10);
|
| 1196 |
+
modalDragOffsetY = parseInt(modalPanel.style.top || 0, 10);
|
| 1197 |
+
e.preventDefault();
|
| 1198 |
+
});
|
| 1199 |
+
|
| 1200 |
+
document.addEventListener('mousemove', (e) => {
|
| 1201 |
+
if (!modalDragging) return;
|
| 1202 |
+
const dx = e.clientX - modalDragStartX;
|
| 1203 |
+
const dy = e.clientY - modalDragStartY;
|
| 1204 |
+
modalPanel.style.left = `${modalDragOffsetX + dx}px`;
|
| 1205 |
+
modalPanel.style.top = `${modalDragOffsetY + dy}px`;
|
| 1206 |
+
});
|
| 1207 |
+
|
| 1208 |
+
document.addEventListener('mouseup', () => {
|
| 1209 |
+
if (modalDragging) {
|
| 1210 |
+
modalDragging = false;
|
| 1211 |
+
modalCustomPos = {
|
| 1212 |
+
x: parseInt(modalPanel.style.left, 10),
|
| 1213 |
+
y: parseInt(modalPanel.style.top, 10),
|
| 1214 |
+
};
|
| 1215 |
+
}
|
| 1216 |
+
});
|
| 1217 |
+
}
|
| 1218 |
}
|
| 1219 |
|
| 1220 |
function onModalKeydown(e) {
|
| 1221 |
+
if (e.key === 'Escape' && modalPanel?.classList.contains('bayan-il-modal-panel--visible')) {
|
| 1222 |
hideModal();
|
| 1223 |
}
|
| 1224 |
}
|
|
|
|
| 1226 |
function showModal() {
|
| 1227 |
createModal();
|
| 1228 |
|
|
|
|
| 1229 |
modalPanel.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1230 |
|
|
|
|
| 1231 |
updateModalScore();
|
| 1232 |
renderModalSuggestions();
|
| 1233 |
|
| 1234 |
+
// Position modal
|
| 1235 |
+
if (!modalCustomPos) {
|
| 1236 |
+
if (activeField) {
|
| 1237 |
+
const fieldRect = activeField.getBoundingClientRect();
|
| 1238 |
+
modalPanel.style.position = 'fixed';
|
| 1239 |
+
let left = fieldRect.left + (fieldRect.width / 2) - 180;
|
| 1240 |
+
if (left < 10) left = 10;
|
| 1241 |
+
if (left + 360 > window.innerWidth) left = window.innerWidth - 370;
|
| 1242 |
+
modalPanel.style.left = `${left}px`;
|
| 1243 |
+
|
| 1244 |
+
let top = fieldRect.bottom + 10;
|
| 1245 |
+
if (top + 400 > window.innerHeight) {
|
| 1246 |
+
top = Math.max(10, window.innerHeight - 410);
|
| 1247 |
+
}
|
| 1248 |
+
modalPanel.style.top = `${top}px`;
|
| 1249 |
+
} else if (floatingBtn) {
|
| 1250 |
+
const fabRect = floatingBtn.getBoundingClientRect();
|
| 1251 |
+
modalPanel.style.position = 'fixed';
|
| 1252 |
+
let left = fabRect.left - 370;
|
| 1253 |
+
if (left < 10) left = 10;
|
| 1254 |
+
modalPanel.style.left = `${left}px`;
|
| 1255 |
+
|
| 1256 |
+
let top = fabRect.bottom - 400;
|
| 1257 |
+
if (top < 10) top = 10;
|
| 1258 |
+
modalPanel.style.top = `${top}px`;
|
| 1259 |
+
}
|
| 1260 |
+
} else {
|
| 1261 |
+
modalPanel.style.position = 'fixed';
|
| 1262 |
+
modalPanel.style.left = `${modalCustomPos.x}px`;
|
| 1263 |
+
modalPanel.style.top = `${modalCustomPos.y}px`;
|
| 1264 |
+
}
|
| 1265 |
+
|
| 1266 |
requestAnimationFrame(() => {
|
| 1267 |
modalPanel.classList.add('bayan-il-modal-panel--visible');
|
| 1268 |
});
|
|
|
|
| 1270 |
|
| 1271 |
function hideModal() {
|
| 1272 |
if (modalPanel) modalPanel.classList.remove('bayan-il-modal-panel--visible');
|
|
|
|
| 1273 |
}
|
| 1274 |
|
| 1275 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1276 |
|
| 1277 |
function updateModalScore() {
|
| 1278 |
if (!modalPanel) return;
|
|
|
|
| 1284 |
|
| 1285 |
const score = calculateWritingScore(counts.spelling, counts.grammar, counts.punctuation);
|
| 1286 |
const color = getScoreColor(score);
|
| 1287 |
+
const offset = 439.8 - (439.8 * score) / 100;
|
| 1288 |
|
| 1289 |
const circle = modalPanel.querySelector('#bayan-modal-score-circle');
|
| 1290 |
const valueEl = modalPanel.querySelector('#bayan-modal-score-value');
|
|
|
|
| 1296 |
circle.setAttribute('stroke-dashoffset', String(offset));
|
| 1297 |
});
|
| 1298 |
}
|
| 1299 |
+
if (valueEl) {
|
| 1300 |
+
const total = counts.spelling + counts.grammar + counts.punctuation;
|
| 1301 |
+
valueEl.textContent = total > 0 ? String(score) : '--';
|
| 1302 |
+
}
|
| 1303 |
if (hintEl) hintEl.textContent = getScoreHint(score);
|
| 1304 |
|
| 1305 |
const toArabicNum = (n) => String(n).replace(/\d/g, (d) => '٠١٢٣٤٥٦٧٨٩'[d]);
|
|
|
|
| 1334 |
}
|
| 1335 |
|
| 1336 |
const toArabicNum = (n) => String(n).replace(/\d/g, (d) => '٠١٢٣٤٥٦٧٨٩'[d]);
|
| 1337 |
+
if (countEl) countEl.textContent = toArabicNum(suggestions.length);
|
| 1338 |
if (applyAllBtn) applyAllBtn.style.display = suggestions.length >= 2 ? 'flex' : 'none';
|
| 1339 |
|
| 1340 |
let html = '';
|
|
|
|
| 1342 |
const alts = resolveAlternatives(s);
|
| 1343 |
const typeLabel = TYPE_LABELS[s.type] || s.type;
|
| 1344 |
|
| 1345 |
+
html += `<div class="bayan-il-modal-card bayan-il-modal-card--${s.type}" data-suggestion-type="${s.type}" data-modal-idx="${idx}">`;
|
| 1346 |
html += `<span class="bayan-il-modal-card-badge bayan-il-modal-badge--${s.type}">${typeLabel}</span>`;
|
| 1347 |
html += `<div class="bayan-il-modal-card-change">`;
|
| 1348 |
html += `<span class="bayan-il-modal-card-original">${esc(s.original)}</span>`;
|
|
|
|
| 1354 |
alts.forEach((alt, ai) => {
|
| 1355 |
const isMain = alt === s.correction && ai === 0;
|
| 1356 |
const isKeep = alt === s.original;
|
|
|
|
|
|
|
|
|
|
| 1357 |
const label = isKeep ? 'إبقاء الأصل' : esc(alt);
|
| 1358 |
+
const cls = isMain ? 'bayan-il-modal-alt-chip bayan-il-modal-alt-chip--main' : (isKeep ? 'bayan-il-modal-alt-chip bayan-il-modal-alt-chip--keep' : 'bayan-il-modal-alt-chip');
|
| 1359 |
html += `<button class="${cls}" data-modal-alt="${esc(alt)}" data-modal-sidx="${idx}" data-modal-keep="${isKeep ? '1' : ''}">${label}</button>`;
|
| 1360 |
});
|
| 1361 |
|
|
|
|
| 1410 |
currentBayanTheme = res.theme || 'dark';
|
| 1411 |
if (typeof tooltip !== 'undefined' && tooltip) tooltip.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1412 |
if (typeof floatingBtn !== 'undefined' && floatingBtn) floatingBtn.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1413 |
+
if (modalPanel) { modalPanel.setAttribute('data-bayan-theme', currentBayanTheme); }
|
|
|
|
| 1414 |
});
|
| 1415 |
|
| 1416 |
chrome.storage.onChanged.addListener((changes, namespace) => {
|
|
|
|
| 1418 |
currentBayanTheme = changes.theme.newValue;
|
| 1419 |
if (typeof tooltip !== 'undefined' && tooltip) tooltip.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1420 |
if (typeof floatingBtn !== 'undefined' && floatingBtn) floatingBtn.setAttribute('data-bayan-theme', currentBayanTheme);
|
| 1421 |
+
if (modalPanel) { modalPanel.setAttribute('data-bayan-theme', currentBayanTheme); }
|
|
|
|
| 1422 |
}
|
| 1423 |
});
|
| 1424 |
|
extension/content.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Bayan Chrome Extension — Content Script
|
| 3 |
-
*
|
| 4 |
-
* Phase 1: Minimal stub — no DOM injection yet.
|
| 5 |
-
* Phase 5: Will detect textarea/contenteditable and show floating icon.
|
| 6 |
-
* Phase 6: Will apply suggestion patches to external page fields.
|
| 7 |
-
* Phase 7: Will add live analysis with debounce.
|
| 8 |
-
*/
|
| 9 |
-
|
| 10 |
-
// Content script loaded — confirm injection
|
| 11 |
-
console.log('[Bayan] Content script loaded on:', window.location.hostname);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/manifest.json
CHANGED
|
@@ -42,6 +42,16 @@
|
|
| 42 |
"https://bayan10-bayan-api.hf.space/*"
|
| 43 |
],
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
"content_scripts": [
|
| 46 |
{
|
| 47 |
"matches": ["https://*/*", "http://*/*"],
|
|
|
|
| 42 |
"https://bayan10-bayan-api.hf.space/*"
|
| 43 |
],
|
| 44 |
|
| 45 |
+
"web_accessible_resources": [
|
| 46 |
+
{
|
| 47 |
+
"resources": [
|
| 48 |
+
"assets/icons/fab_logo.png",
|
| 49 |
+
"assets/icons/icon128.png"
|
| 50 |
+
],
|
| 51 |
+
"matches": ["<all_urls>"]
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
|
| 55 |
"content_scripts": [
|
| 56 |
{
|
| 57 |
"matches": ["https://*/*", "http://*/*"],
|
extension/popup.css
CHANGED
|
@@ -3,41 +3,42 @@
|
|
| 3 |
Arabic-first RTL | Dark mode | Premium feel
|
| 4 |
══════════════════════════════════════════════ */
|
| 5 |
|
| 6 |
-
/* ── Design Tokens ── */
|
| 7 |
:root {
|
| 8 |
-
--bayan-primary: #
|
| 9 |
-
--bayan-primary-light: #
|
| 10 |
-
--bayan-primary-dark: #
|
| 11 |
-
--bayan-primary-glow: rgba(
|
| 12 |
-
|
| 13 |
-
--bayan-bg: #
|
| 14 |
-
--bayan-surface: #
|
| 15 |
-
--bayan-surface-hover: #
|
| 16 |
-
--bayan-surface-active: #
|
| 17 |
-
--bayan-border:
|
| 18 |
-
--bayan-border-light:
|
| 19 |
-
|
| 20 |
-
--bayan-text: #
|
| 21 |
-
--bayan-text-secondary: #
|
| 22 |
-
--bayan-text-muted: #
|
| 23 |
-
|
| 24 |
-
--bayan-success: #
|
| 25 |
-
--bayan-warning: #
|
| 26 |
-
--bayan-error: #
|
| 27 |
-
|
| 28 |
-
--bayan-spelling: #
|
| 29 |
-
--bayan-grammar: #
|
| 30 |
-
--bayan-punctuation: #6BC98A;
|
| 31 |
|
| 32 |
--bayan-radius: 10px;
|
| 33 |
--bayan-radius-sm: 6px;
|
| 34 |
--bayan-radius-lg: 14px;
|
| 35 |
|
| 36 |
-
--bayan-font:
|
| 37 |
-
--bayan-font-arabic: 'Noto Sans Arabic', 'Segoe UI', Tahoma, sans-serif;
|
| 38 |
|
| 39 |
--bayan-transition: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
| 40 |
--bayan-spring: 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
/* ── Reset ── */
|
|
@@ -56,8 +57,8 @@ body {
|
|
| 56 |
font-family: var(--bayan-font-arabic);
|
| 57 |
font-size: 14px;
|
| 58 |
line-height: 1.6;
|
| 59 |
-
color: var(--
|
| 60 |
-
background: var(--
|
| 61 |
direction: rtl;
|
| 62 |
-webkit-font-smoothing: antialiased;
|
| 63 |
}
|
|
@@ -70,7 +71,7 @@ body::-webkit-scrollbar-track {
|
|
| 70 |
background: transparent;
|
| 71 |
}
|
| 72 |
body::-webkit-scrollbar-thumb {
|
| 73 |
-
background: var(--
|
| 74 |
border-radius: 4px;
|
| 75 |
}
|
| 76 |
|
|
@@ -82,8 +83,8 @@ body::-webkit-scrollbar-thumb {
|
|
| 82 |
align-items: center;
|
| 83 |
justify-content: space-between;
|
| 84 |
padding: 12px 16px;
|
| 85 |
-
background: linear-gradient(135deg, var(--
|
| 86 |
-
border-bottom: 1px solid var(--
|
| 87 |
}
|
| 88 |
|
| 89 |
.bayan-header-brand {
|
|
@@ -99,7 +100,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 99 |
.bayan-header-title {
|
| 100 |
font-size: 18px;
|
| 101 |
font-weight: 700;
|
| 102 |
-
background: linear-gradient(135deg, var(--
|
| 103 |
-webkit-background-clip: text;
|
| 104 |
-webkit-text-fill-color: transparent;
|
| 105 |
background-clip: text;
|
|
@@ -123,11 +124,11 @@ body::-webkit-scrollbar-thumb {
|
|
| 123 |
}
|
| 124 |
.bayan-status-dot.online {
|
| 125 |
background: var(--bayan-success);
|
| 126 |
-
box-shadow: 0 0 6px rgba(
|
| 127 |
}
|
| 128 |
.bayan-status-dot.offline {
|
| 129 |
background: var(--bayan-error);
|
| 130 |
-
box-shadow: 0 0 6px rgba(
|
| 131 |
}
|
| 132 |
|
| 133 |
/* ══════════════════════════════════════════════
|
|
@@ -136,27 +137,28 @@ body::-webkit-scrollbar-thumb {
|
|
| 136 |
.bayan-tabs {
|
| 137 |
display: flex;
|
| 138 |
gap: 0;
|
| 139 |
-
border-bottom: 1px solid var(--
|
| 140 |
-
background: var(--
|
| 141 |
}
|
| 142 |
|
| 143 |
.bayan-tab {
|
| 144 |
-
flex: 1;
|
| 145 |
display: flex;
|
| 146 |
align-items: center;
|
| 147 |
justify-content: center;
|
| 148 |
gap: 4px;
|
| 149 |
-
padding: 10px
|
| 150 |
border: none;
|
| 151 |
background: transparent;
|
| 152 |
color: var(--bayan-text-muted);
|
| 153 |
font-family: var(--bayan-font-arabic);
|
| 154 |
-
font-size:
|
| 155 |
font-weight: 500;
|
| 156 |
cursor: pointer;
|
| 157 |
transition: all var(--bayan-transition);
|
| 158 |
position: relative;
|
| 159 |
white-space: nowrap;
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
.bayan-tab::after {
|
|
@@ -166,7 +168,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 166 |
left: 20%;
|
| 167 |
right: 20%;
|
| 168 |
height: 2px;
|
| 169 |
-
background: var(--
|
| 170 |
border-radius: 2px 2px 0 0;
|
| 171 |
opacity: 0;
|
| 172 |
transform: scaleX(0);
|
|
@@ -174,12 +176,12 @@ body::-webkit-scrollbar-thumb {
|
|
| 174 |
}
|
| 175 |
|
| 176 |
.bayan-tab:hover {
|
| 177 |
-
color: var(--
|
| 178 |
-
background: var(--
|
| 179 |
}
|
| 180 |
|
| 181 |
.bayan-tab.active {
|
| 182 |
-
color: var(--
|
| 183 |
}
|
| 184 |
.bayan-tab.active::after {
|
| 185 |
opacity: 1;
|
|
@@ -216,22 +218,27 @@ body::-webkit-scrollbar-thumb {
|
|
| 216 |
margin-bottom: 12px;
|
| 217 |
}
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
.bayan-label {
|
| 220 |
-
display: block;
|
| 221 |
font-size: 12px;
|
| 222 |
font-weight: 600;
|
| 223 |
-
color: var(--
|
| 224 |
-
margin-bottom: 6px;
|
| 225 |
}
|
| 226 |
|
| 227 |
.bayan-textarea {
|
| 228 |
width: 100%;
|
| 229 |
min-height: 100px;
|
| 230 |
padding: 12px 14px;
|
| 231 |
-
border: 1px solid var(--
|
| 232 |
border-radius: var(--bayan-radius);
|
| 233 |
-
background: var(--
|
| 234 |
-
color: var(--
|
| 235 |
font-family: var(--bayan-font-arabic);
|
| 236 |
font-size: 14px;
|
| 237 |
line-height: 1.8;
|
|
@@ -242,7 +249,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 242 |
|
| 243 |
.bayan-textarea:focus {
|
| 244 |
outline: none;
|
| 245 |
-
border-color: var(--
|
| 246 |
box-shadow: 0 0 0 3px var(--bayan-primary-glow);
|
| 247 |
}
|
| 248 |
|
|
@@ -289,33 +296,33 @@ body::-webkit-scrollbar-thumb {
|
|
| 289 |
|
| 290 |
.bayan-btn-primary {
|
| 291 |
flex: 1;
|
| 292 |
-
background: linear-gradient(135deg, var(--
|
| 293 |
color: white;
|
| 294 |
-
box-shadow: 0 2px 8px rgba(
|
| 295 |
}
|
| 296 |
.bayan-btn-primary:hover {
|
| 297 |
-
box-shadow: 0 4px 16px rgba(
|
| 298 |
transform: translateY(-1px);
|
| 299 |
}
|
| 300 |
.bayan-btn-primary:active {
|
| 301 |
transform: translateY(0);
|
| 302 |
-
box-shadow: 0 1px 4px rgba(
|
| 303 |
}
|
| 304 |
|
| 305 |
.bayan-btn-ghost {
|
| 306 |
background: transparent;
|
| 307 |
-
color: var(--
|
| 308 |
-
border: 1px solid var(--
|
| 309 |
}
|
| 310 |
.bayan-btn-ghost:hover {
|
| 311 |
-
background: var(--
|
| 312 |
-
color: var(--
|
| 313 |
}
|
| 314 |
|
| 315 |
.bayan-btn-sm {
|
| 316 |
padding: 5px 12px;
|
| 317 |
font-size: 11px;
|
| 318 |
-
background: var(--
|
| 319 |
color: white;
|
| 320 |
border-radius: var(--bayan-radius-sm);
|
| 321 |
}
|
|
@@ -338,8 +345,8 @@ body::-webkit-scrollbar-thumb {
|
|
| 338 |
transition: all var(--bayan-transition);
|
| 339 |
}
|
| 340 |
.bayan-btn-icon:hover {
|
| 341 |
-
background: var(--
|
| 342 |
-
color: var(--
|
| 343 |
}
|
| 344 |
|
| 345 |
/* ══════════════════════════════════════════════
|
|
@@ -347,21 +354,30 @@ body::-webkit-scrollbar-thumb {
|
|
| 347 |
══════════════════════════════════════════════ */
|
| 348 |
.bayan-score-section {
|
| 349 |
display: flex;
|
|
|
|
| 350 |
align-items: center;
|
| 351 |
-
|
| 352 |
-
padding: 12px;
|
| 353 |
margin-bottom: 14px;
|
| 354 |
-
background: var(--
|
| 355 |
-
border: 1px solid var(--
|
| 356 |
border-radius: var(--bayan-radius);
|
| 357 |
animation: fadeSlideIn 300ms ease-out;
|
| 358 |
}
|
| 359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
.bayan-score-ring {
|
| 361 |
position: relative;
|
| 362 |
-
width:
|
| 363 |
-
height:
|
| 364 |
flex-shrink: 0;
|
|
|
|
| 365 |
}
|
| 366 |
|
| 367 |
.bayan-score-ring svg {
|
|
@@ -378,19 +394,15 @@ body::-webkit-scrollbar-thumb {
|
|
| 378 |
display: flex;
|
| 379 |
align-items: center;
|
| 380 |
justify-content: center;
|
| 381 |
-
font-size:
|
| 382 |
font-weight: 700;
|
| 383 |
-
color: var(--
|
| 384 |
-
}
|
| 385 |
-
|
| 386 |
-
.bayan-score-meta {
|
| 387 |
-
flex: 1;
|
| 388 |
}
|
| 389 |
|
| 390 |
.bayan-score-hint {
|
| 391 |
display: block;
|
| 392 |
font-size: 12px;
|
| 393 |
-
color: var(--
|
| 394 |
margin-bottom: 8px;
|
| 395 |
}
|
| 396 |
|
|
@@ -398,6 +410,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 398 |
display: flex;
|
| 399 |
gap: 10px;
|
| 400 |
flex-wrap: wrap;
|
|
|
|
| 401 |
}
|
| 402 |
|
| 403 |
.bayan-count {
|
|
@@ -432,24 +445,24 @@ body::-webkit-scrollbar-thumb {
|
|
| 432 |
.bayan-result-title {
|
| 433 |
font-size: 12px;
|
| 434 |
font-weight: 600;
|
| 435 |
-
color: var(--
|
| 436 |
}
|
| 437 |
|
| 438 |
.bayan-result-text {
|
| 439 |
padding: 12px 14px;
|
| 440 |
-
background: var(--
|
| 441 |
-
border: 1px solid var(--
|
| 442 |
border-radius: var(--bayan-radius);
|
| 443 |
font-size: 14px;
|
| 444 |
line-height: 2;
|
| 445 |
-
color: var(--
|
| 446 |
min-height: 60px;
|
| 447 |
word-wrap: break-word;
|
| 448 |
}
|
| 449 |
|
| 450 |
/* Error highlights in result */
|
| 451 |
.bayan-spelling-error {
|
| 452 |
-
background: rgba(
|
| 453 |
border-bottom: 2px solid var(--bayan-spelling);
|
| 454 |
border-radius: 2px;
|
| 455 |
padding: 0 2px;
|
|
@@ -457,11 +470,11 @@ body::-webkit-scrollbar-thumb {
|
|
| 457 |
transition: background var(--bayan-transition);
|
| 458 |
}
|
| 459 |
.bayan-spelling-error:hover {
|
| 460 |
-
background: rgba(
|
| 461 |
}
|
| 462 |
|
| 463 |
.bayan-grammar-error {
|
| 464 |
-
background: rgba(
|
| 465 |
border-bottom: 2px solid var(--bayan-grammar);
|
| 466 |
border-radius: 2px;
|
| 467 |
padding: 0 2px;
|
|
@@ -469,7 +482,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 469 |
transition: background var(--bayan-transition);
|
| 470 |
}
|
| 471 |
.bayan-grammar-error:hover {
|
| 472 |
-
background: rgba(
|
| 473 |
}
|
| 474 |
|
| 475 |
.bayan-punctuation-suggestion {
|
|
@@ -501,7 +514,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 501 |
.bayan-suggestions-title {
|
| 502 |
font-size: 12px;
|
| 503 |
font-weight: 600;
|
| 504 |
-
color: var(--
|
| 505 |
}
|
| 506 |
|
| 507 |
.bayan-suggestions-list {
|
|
@@ -526,15 +539,15 @@ body::-webkit-scrollbar-thumb {
|
|
| 526 |
|
| 527 |
.bayan-suggestion-card {
|
| 528 |
padding: 10px 12px;
|
| 529 |
-
background: var(--
|
| 530 |
-
border: 1px solid var(--
|
| 531 |
border-radius: var(--bayan-radius);
|
| 532 |
cursor: pointer;
|
| 533 |
transition: all var(--bayan-transition);
|
| 534 |
}
|
| 535 |
.bayan-suggestion-card:hover {
|
| 536 |
-
border-color: var(--
|
| 537 |
-
background: var(--
|
| 538 |
box-shadow: 0 0 0 1px var(--bayan-primary-glow);
|
| 539 |
}
|
| 540 |
|
|
@@ -548,15 +561,15 @@ body::-webkit-scrollbar-thumb {
|
|
| 548 |
}
|
| 549 |
|
| 550 |
.bayan-badge-spelling {
|
| 551 |
-
background: rgba(
|
| 552 |
color: var(--bayan-spelling);
|
| 553 |
}
|
| 554 |
.bayan-badge-grammar {
|
| 555 |
-
background: rgba(
|
| 556 |
color: var(--bayan-grammar);
|
| 557 |
}
|
| 558 |
.bayan-badge-punctuation {
|
| 559 |
-
background: rgba(107, 201, 138, 0.
|
| 560 |
color: var(--bayan-punctuation);
|
| 561 |
}
|
| 562 |
|
|
@@ -592,24 +605,24 @@ body::-webkit-scrollbar-thumb {
|
|
| 592 |
|
| 593 |
.bayan-alt-chip {
|
| 594 |
padding: 4px 10px;
|
| 595 |
-
border: 1px solid var(--
|
| 596 |
border-radius: 20px;
|
| 597 |
background: transparent;
|
| 598 |
-
color: var(--
|
| 599 |
font-family: var(--bayan-font-arabic);
|
| 600 |
font-size: 11px;
|
| 601 |
cursor: pointer;
|
| 602 |
transition: all var(--bayan-transition);
|
| 603 |
}
|
| 604 |
.bayan-alt-chip:hover {
|
| 605 |
-
border-color: var(--
|
| 606 |
-
color: var(--
|
| 607 |
background: var(--bayan-surface-active);
|
| 608 |
}
|
| 609 |
|
| 610 |
.bayan-alt-chip--main {
|
| 611 |
-
background: var(--
|
| 612 |
-
border-color: var(--
|
| 613 |
color: white;
|
| 614 |
}
|
| 615 |
.bayan-alt-chip--main:hover {
|
|
@@ -630,7 +643,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 630 |
.bayan-timing {
|
| 631 |
padding: 8px 0;
|
| 632 |
margin-top: 8px;
|
| 633 |
-
border-top: 1px solid var(--
|
| 634 |
font-size: 10px;
|
| 635 |
color: var(--bayan-text-muted);
|
| 636 |
text-align: center;
|
|
@@ -642,12 +655,178 @@ body::-webkit-scrollbar-thumb {
|
|
| 642 |
.bayan-summary-meta {
|
| 643 |
margin-top: 10px;
|
| 644 |
padding: 8px 12px;
|
| 645 |
-
background: var(--
|
| 646 |
border-radius: var(--bayan-radius-sm);
|
| 647 |
font-size: 11px;
|
| 648 |
color: var(--bayan-text-muted);
|
| 649 |
}
|
| 650 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
/* ══════════════════════════════════════════════
|
| 652 |
Length Selector
|
| 653 |
══════════════════════════════════════════════ */
|
|
@@ -665,12 +844,12 @@ body::-webkit-scrollbar-thumb {
|
|
| 665 |
align-items: center;
|
| 666 |
gap: 4px;
|
| 667 |
font-size: 12px;
|
| 668 |
-
color: var(--
|
| 669 |
cursor: pointer;
|
| 670 |
}
|
| 671 |
|
| 672 |
.bayan-radio input[type="radio"] {
|
| 673 |
-
accent-color: var(--
|
| 674 |
width: 14px;
|
| 675 |
height: 14px;
|
| 676 |
cursor: pointer;
|
|
@@ -687,7 +866,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 687 |
align-items: center;
|
| 688 |
justify-content: center;
|
| 689 |
gap: 14px;
|
| 690 |
-
background: rgba(15, 15, 20, 0.85);
|
| 691 |
backdrop-filter: blur(4px);
|
| 692 |
z-index: 100;
|
| 693 |
}
|
|
@@ -695,8 +874,8 @@ body::-webkit-scrollbar-thumb {
|
|
| 695 |
.bayan-spinner {
|
| 696 |
width: 36px;
|
| 697 |
height: 36px;
|
| 698 |
-
border: 3px solid var(--
|
| 699 |
-
border-top-color: var(--
|
| 700 |
border-radius: 50%;
|
| 701 |
animation: spin 0.8s linear infinite;
|
| 702 |
}
|
|
@@ -707,7 +886,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 707 |
|
| 708 |
.bayan-loading-text {
|
| 709 |
font-size: 13px;
|
| 710 |
-
color: var(--
|
| 711 |
}
|
| 712 |
|
| 713 |
/* ══════════════════════════════════════════════
|
|
@@ -719,10 +898,10 @@ body::-webkit-scrollbar-thumb {
|
|
| 719 |
left: 50%;
|
| 720 |
transform: translateX(-50%);
|
| 721 |
padding: 8px 18px;
|
| 722 |
-
background: var(--
|
| 723 |
-
border: 1px solid var(--
|
| 724 |
border-radius: 20px;
|
| 725 |
-
color: var(--
|
| 726 |
font-size: 12px;
|
| 727 |
font-family: var(--bayan-font-arabic);
|
| 728 |
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
|
@@ -761,6 +940,87 @@ body::-webkit-scrollbar-thumb {
|
|
| 761 |
white-space: nowrap;
|
| 762 |
}
|
| 763 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
/* ══════════════════════════════════════════════
|
| 765 |
Utilities
|
| 766 |
══════════════════════════════════════════════ */
|
|
@@ -769,20 +1029,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 769 |
}
|
| 770 |
|
| 771 |
|
| 772 |
-
/* Light
|
| 773 |
-
[data-theme="light"] {
|
| 774 |
-
--bayan-bg: #f9fafb;
|
| 775 |
-
--bayan-surface: #ffffff;
|
| 776 |
-
--bayan-surface-hover: #f3f4f6;
|
| 777 |
-
--bayan-surface-active: #e5e7eb;
|
| 778 |
-
--bayan-border: #e5e7eb;
|
| 779 |
-
--bayan-border-light: #d1d5db;
|
| 780 |
-
--bayan-text: #111827;
|
| 781 |
-
--bayan-text-secondary: #4b5563;
|
| 782 |
-
--bayan-text-muted: #9ca3af;
|
| 783 |
-
--bayan-success: #16a34a;
|
| 784 |
-
--bayan-warning: #d97706;
|
| 785 |
-
}
|
| 786 |
|
| 787 |
/* Theme Toggle Button Styles */
|
| 788 |
.theme-toggle-animated {
|
|
@@ -793,8 +1040,8 @@ body::-webkit-scrollbar-thumb {
|
|
| 793 |
height: 32px;
|
| 794 |
border: none;
|
| 795 |
border-radius: 50%;
|
| 796 |
-
background: var(--
|
| 797 |
-
color: var(--
|
| 798 |
cursor: pointer;
|
| 799 |
transition: background 0.3s ease, transform 0.3s ease, color 0.3s ease;
|
| 800 |
position: relative;
|
|
@@ -803,7 +1050,7 @@ body::-webkit-scrollbar-thumb {
|
|
| 803 |
}
|
| 804 |
|
| 805 |
.theme-toggle-animated:hover {
|
| 806 |
-
background: var(--
|
| 807 |
color: #fff;
|
| 808 |
transform: rotate(15deg);
|
| 809 |
}
|
|
|
|
| 3 |
Arabic-first RTL | Dark mode | Premium feel
|
| 4 |
══════════════════════════════════════════════ */
|
| 5 |
|
| 6 |
+
/* ── Design Tokens (mapped to website tokens.css) ── */
|
| 7 |
:root {
|
| 8 |
+
--bayan-primary: var(--color-primary, #6BA3E0);
|
| 9 |
+
--bayan-primary-light: #8BB8E8;
|
| 10 |
+
--bayan-primary-dark: #5A8FCA;
|
| 11 |
+
--bayan-primary-glow: var(--focus-ring, rgba(107, 163, 224, 0.25));
|
| 12 |
+
|
| 13 |
+
--bayan-bg: var(--color-bg, #12141A);
|
| 14 |
+
--bayan-surface: var(--color-surface, #1A1D26);
|
| 15 |
+
--bayan-surface-hover: var(--color-surface-elevated, #242833);
|
| 16 |
+
--bayan-surface-active: #2C3040;
|
| 17 |
+
--bayan-border: var(--color-border);
|
| 18 |
+
--bayan-border-light: var(--color-border-strong);
|
| 19 |
+
|
| 20 |
+
--bayan-text: var(--color-text-primary, #ECEEF2);
|
| 21 |
+
--bayan-text-secondary: var(--color-text-secondary, #B4BBC6);
|
| 22 |
+
--bayan-text-muted: var(--color-text-muted, #8A939F);
|
| 23 |
+
|
| 24 |
+
--bayan-success: var(--color-success, #6BC98A);
|
| 25 |
+
--bayan-warning: var(--color-warning, #E4B35A);
|
| 26 |
+
--bayan-error: var(--color-error, #E88A8A);
|
| 27 |
+
|
| 28 |
+
--bayan-spelling: var(--highlight-spelling-border, #E88A8A);
|
| 29 |
+
--bayan-grammar: var(--highlight-grammar-border, #E4B35A);
|
| 30 |
+
--bayan-punctuation: var(--highlight-punctuation-border, #6BC98A);
|
| 31 |
|
| 32 |
--bayan-radius: 10px;
|
| 33 |
--bayan-radius-sm: 6px;
|
| 34 |
--bayan-radius-lg: 14px;
|
| 35 |
|
| 36 |
+
--bayan-font-arabic: var(--font-family-primary, 'Cairo', 'Tajawal', 'Noto Sans Arabic', sans-serif);
|
|
|
|
| 37 |
|
| 38 |
--bayan-transition: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
| 39 |
--bayan-spring: 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 40 |
+
|
| 41 |
+
--color-surface-hover: var(--color-surface-elevated, #242833);
|
| 42 |
}
|
| 43 |
|
| 44 |
/* ── Reset ── */
|
|
|
|
| 57 |
font-family: var(--bayan-font-arabic);
|
| 58 |
font-size: 14px;
|
| 59 |
line-height: 1.6;
|
| 60 |
+
color: var(--text-color);
|
| 61 |
+
background: var(--color-bg);
|
| 62 |
direction: rtl;
|
| 63 |
-webkit-font-smoothing: antialiased;
|
| 64 |
}
|
|
|
|
| 71 |
background: transparent;
|
| 72 |
}
|
| 73 |
body::-webkit-scrollbar-thumb {
|
| 74 |
+
background: var(--color-border);
|
| 75 |
border-radius: 4px;
|
| 76 |
}
|
| 77 |
|
|
|
|
| 83 |
align-items: center;
|
| 84 |
justify-content: space-between;
|
| 85 |
padding: 12px 16px;
|
| 86 |
+
background: linear-gradient(135deg, var(--color-surface) 0%, var(--color-bg) 100%);
|
| 87 |
+
border-bottom: 1px solid var(--color-border);
|
| 88 |
}
|
| 89 |
|
| 90 |
.bayan-header-brand {
|
|
|
|
| 100 |
.bayan-header-title {
|
| 101 |
font-size: 18px;
|
| 102 |
font-weight: 700;
|
| 103 |
+
background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
|
| 104 |
-webkit-background-clip: text;
|
| 105 |
-webkit-text-fill-color: transparent;
|
| 106 |
background-clip: text;
|
|
|
|
| 124 |
}
|
| 125 |
.bayan-status-dot.online {
|
| 126 |
background: var(--bayan-success);
|
| 127 |
+
box-shadow: 0 0 6px rgba(107, 201, 138, 0.5);
|
| 128 |
}
|
| 129 |
.bayan-status-dot.offline {
|
| 130 |
background: var(--bayan-error);
|
| 131 |
+
box-shadow: 0 0 6px rgba(232, 138, 138, 0.4);
|
| 132 |
}
|
| 133 |
|
| 134 |
/* ══════════════════════════════════════════════
|
|
|
|
| 137 |
.bayan-tabs {
|
| 138 |
display: flex;
|
| 139 |
gap: 0;
|
| 140 |
+
border-bottom: 1px solid var(--color-border);
|
| 141 |
+
background: var(--color-surface);
|
| 142 |
}
|
| 143 |
|
| 144 |
.bayan-tab {
|
| 145 |
+
flex: 1 1 auto;
|
| 146 |
display: flex;
|
| 147 |
align-items: center;
|
| 148 |
justify-content: center;
|
| 149 |
gap: 4px;
|
| 150 |
+
padding: 10px 6px;
|
| 151 |
border: none;
|
| 152 |
background: transparent;
|
| 153 |
color: var(--bayan-text-muted);
|
| 154 |
font-family: var(--bayan-font-arabic);
|
| 155 |
+
font-size: 11px;
|
| 156 |
font-weight: 500;
|
| 157 |
cursor: pointer;
|
| 158 |
transition: all var(--bayan-transition);
|
| 159 |
position: relative;
|
| 160 |
white-space: nowrap;
|
| 161 |
+
min-width: 0;
|
| 162 |
}
|
| 163 |
|
| 164 |
.bayan-tab::after {
|
|
|
|
| 168 |
left: 20%;
|
| 169 |
right: 20%;
|
| 170 |
height: 2px;
|
| 171 |
+
background: var(--primary-color);
|
| 172 |
border-radius: 2px 2px 0 0;
|
| 173 |
opacity: 0;
|
| 174 |
transform: scaleX(0);
|
|
|
|
| 176 |
}
|
| 177 |
|
| 178 |
.bayan-tab:hover {
|
| 179 |
+
color: var(--text-color);
|
| 180 |
+
background: var(--color-surface-hover);
|
| 181 |
}
|
| 182 |
|
| 183 |
.bayan-tab.active {
|
| 184 |
+
color: var(--secondary-color);
|
| 185 |
}
|
| 186 |
.bayan-tab.active::after {
|
| 187 |
opacity: 1;
|
|
|
|
| 218 |
margin-bottom: 12px;
|
| 219 |
}
|
| 220 |
|
| 221 |
+
.bayan-label-row {
|
| 222 |
+
display: flex;
|
| 223 |
+
align-items: center;
|
| 224 |
+
justify-content: space-between;
|
| 225 |
+
margin-bottom: 6px;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
.bayan-label {
|
|
|
|
| 229 |
font-size: 12px;
|
| 230 |
font-weight: 600;
|
| 231 |
+
color: var(--text-secondary);
|
|
|
|
| 232 |
}
|
| 233 |
|
| 234 |
.bayan-textarea {
|
| 235 |
width: 100%;
|
| 236 |
min-height: 100px;
|
| 237 |
padding: 12px 14px;
|
| 238 |
+
border: 1px solid var(--color-border);
|
| 239 |
border-radius: var(--bayan-radius);
|
| 240 |
+
background: var(--color-surface);
|
| 241 |
+
color: var(--text-color);
|
| 242 |
font-family: var(--bayan-font-arabic);
|
| 243 |
font-size: 14px;
|
| 244 |
line-height: 1.8;
|
|
|
|
| 249 |
|
| 250 |
.bayan-textarea:focus {
|
| 251 |
outline: none;
|
| 252 |
+
border-color: var(--primary-color);
|
| 253 |
box-shadow: 0 0 0 3px var(--bayan-primary-glow);
|
| 254 |
}
|
| 255 |
|
|
|
|
| 296 |
|
| 297 |
.bayan-btn-primary {
|
| 298 |
flex: 1;
|
| 299 |
+
background: linear-gradient(135deg, var(--color-primary, #6BA3E0), var(--color-secondary, #A594E8));
|
| 300 |
color: white;
|
| 301 |
+
box-shadow: 0 2px 8px rgba(107, 163, 224, 0.3);
|
| 302 |
}
|
| 303 |
.bayan-btn-primary:hover {
|
| 304 |
+
box-shadow: 0 4px 16px rgba(107, 163, 224, 0.45);
|
| 305 |
transform: translateY(-1px);
|
| 306 |
}
|
| 307 |
.bayan-btn-primary:active {
|
| 308 |
transform: translateY(0);
|
| 309 |
+
box-shadow: 0 1px 4px rgba(107, 163, 224, 0.2);
|
| 310 |
}
|
| 311 |
|
| 312 |
.bayan-btn-ghost {
|
| 313 |
background: transparent;
|
| 314 |
+
color: var(--text-secondary);
|
| 315 |
+
border: 1px solid var(--color-border);
|
| 316 |
}
|
| 317 |
.bayan-btn-ghost:hover {
|
| 318 |
+
background: var(--color-surface-hover);
|
| 319 |
+
color: var(--text-color);
|
| 320 |
}
|
| 321 |
|
| 322 |
.bayan-btn-sm {
|
| 323 |
padding: 5px 12px;
|
| 324 |
font-size: 11px;
|
| 325 |
+
background: var(--primary-color);
|
| 326 |
color: white;
|
| 327 |
border-radius: var(--bayan-radius-sm);
|
| 328 |
}
|
|
|
|
| 345 |
transition: all var(--bayan-transition);
|
| 346 |
}
|
| 347 |
.bayan-btn-icon:hover {
|
| 348 |
+
background: var(--color-surface-hover);
|
| 349 |
+
color: var(--text-color);
|
| 350 |
}
|
| 351 |
|
| 352 |
/* ══════════════════════════════════════════════
|
|
|
|
| 354 |
══════════════════════════════════════════════ */
|
| 355 |
.bayan-score-section {
|
| 356 |
display: flex;
|
| 357 |
+
flex-direction: column;
|
| 358 |
align-items: center;
|
| 359 |
+
text-align: center;
|
| 360 |
+
padding: 16px 12px;
|
| 361 |
margin-bottom: 14px;
|
| 362 |
+
background: var(--color-surface);
|
| 363 |
+
border: 1px solid var(--color-border);
|
| 364 |
border-radius: var(--bayan-radius);
|
| 365 |
animation: fadeSlideIn 300ms ease-out;
|
| 366 |
}
|
| 367 |
|
| 368 |
+
.bayan-score-title {
|
| 369 |
+
font-size: 13px;
|
| 370 |
+
font-weight: 600;
|
| 371 |
+
color: var(--text-secondary);
|
| 372 |
+
margin-bottom: 10px;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
.bayan-score-ring {
|
| 376 |
position: relative;
|
| 377 |
+
width: 120px;
|
| 378 |
+
height: 120px;
|
| 379 |
flex-shrink: 0;
|
| 380 |
+
margin-bottom: 8px;
|
| 381 |
}
|
| 382 |
|
| 383 |
.bayan-score-ring svg {
|
|
|
|
| 394 |
display: flex;
|
| 395 |
align-items: center;
|
| 396 |
justify-content: center;
|
| 397 |
+
font-size: 28px;
|
| 398 |
font-weight: 700;
|
| 399 |
+
color: var(--secondary-color);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
}
|
| 401 |
|
| 402 |
.bayan-score-hint {
|
| 403 |
display: block;
|
| 404 |
font-size: 12px;
|
| 405 |
+
color: var(--text-secondary);
|
| 406 |
margin-bottom: 8px;
|
| 407 |
}
|
| 408 |
|
|
|
|
| 410 |
display: flex;
|
| 411 |
gap: 10px;
|
| 412 |
flex-wrap: wrap;
|
| 413 |
+
justify-content: center;
|
| 414 |
}
|
| 415 |
|
| 416 |
.bayan-count {
|
|
|
|
| 445 |
.bayan-result-title {
|
| 446 |
font-size: 12px;
|
| 447 |
font-weight: 600;
|
| 448 |
+
color: var(--text-secondary);
|
| 449 |
}
|
| 450 |
|
| 451 |
.bayan-result-text {
|
| 452 |
padding: 12px 14px;
|
| 453 |
+
background: var(--color-surface);
|
| 454 |
+
border: 1px solid var(--color-border);
|
| 455 |
border-radius: var(--bayan-radius);
|
| 456 |
font-size: 14px;
|
| 457 |
line-height: 2;
|
| 458 |
+
color: var(--text-color);
|
| 459 |
min-height: 60px;
|
| 460 |
word-wrap: break-word;
|
| 461 |
}
|
| 462 |
|
| 463 |
/* Error highlights in result */
|
| 464 |
.bayan-spelling-error {
|
| 465 |
+
background: var(--highlight-spelling-bg, rgba(232, 138, 138, 0.16));
|
| 466 |
border-bottom: 2px solid var(--bayan-spelling);
|
| 467 |
border-radius: 2px;
|
| 468 |
padding: 0 2px;
|
|
|
|
| 470 |
transition: background var(--bayan-transition);
|
| 471 |
}
|
| 472 |
.bayan-spelling-error:hover {
|
| 473 |
+
background: rgba(232, 138, 138, 0.28);
|
| 474 |
}
|
| 475 |
|
| 476 |
.bayan-grammar-error {
|
| 477 |
+
background: var(--highlight-grammar-bg, rgba(228, 179, 90, 0.16));
|
| 478 |
border-bottom: 2px solid var(--bayan-grammar);
|
| 479 |
border-radius: 2px;
|
| 480 |
padding: 0 2px;
|
|
|
|
| 482 |
transition: background var(--bayan-transition);
|
| 483 |
}
|
| 484 |
.bayan-grammar-error:hover {
|
| 485 |
+
background: rgba(228, 179, 90, 0.28);
|
| 486 |
}
|
| 487 |
|
| 488 |
.bayan-punctuation-suggestion {
|
|
|
|
| 514 |
.bayan-suggestions-title {
|
| 515 |
font-size: 12px;
|
| 516 |
font-weight: 600;
|
| 517 |
+
color: var(--text-secondary);
|
| 518 |
}
|
| 519 |
|
| 520 |
.bayan-suggestions-list {
|
|
|
|
| 539 |
|
| 540 |
.bayan-suggestion-card {
|
| 541 |
padding: 10px 12px;
|
| 542 |
+
background: var(--color-surface);
|
| 543 |
+
border: 1px solid var(--color-border);
|
| 544 |
border-radius: var(--bayan-radius);
|
| 545 |
cursor: pointer;
|
| 546 |
transition: all var(--bayan-transition);
|
| 547 |
}
|
| 548 |
.bayan-suggestion-card:hover {
|
| 549 |
+
border-color: var(--primary-color);
|
| 550 |
+
background: var(--color-surface-hover);
|
| 551 |
box-shadow: 0 0 0 1px var(--bayan-primary-glow);
|
| 552 |
}
|
| 553 |
|
|
|
|
| 561 |
}
|
| 562 |
|
| 563 |
.bayan-badge-spelling {
|
| 564 |
+
background: var(--color-badge-spelling-bg, rgba(232, 138, 138, 0.18));
|
| 565 |
color: var(--bayan-spelling);
|
| 566 |
}
|
| 567 |
.bayan-badge-grammar {
|
| 568 |
+
background: var(--color-badge-grammar-bg, rgba(228, 179, 90, 0.18));
|
| 569 |
color: var(--bayan-grammar);
|
| 570 |
}
|
| 571 |
.bayan-badge-punctuation {
|
| 572 |
+
background: var(--color-badge-punctuation-bg, rgba(107, 201, 138, 0.16));
|
| 573 |
color: var(--bayan-punctuation);
|
| 574 |
}
|
| 575 |
|
|
|
|
| 605 |
|
| 606 |
.bayan-alt-chip {
|
| 607 |
padding: 4px 10px;
|
| 608 |
+
border: 1px solid var(--color-border);
|
| 609 |
border-radius: 20px;
|
| 610 |
background: transparent;
|
| 611 |
+
color: var(--text-secondary);
|
| 612 |
font-family: var(--bayan-font-arabic);
|
| 613 |
font-size: 11px;
|
| 614 |
cursor: pointer;
|
| 615 |
transition: all var(--bayan-transition);
|
| 616 |
}
|
| 617 |
.bayan-alt-chip:hover {
|
| 618 |
+
border-color: var(--primary-color);
|
| 619 |
+
color: var(--text-color);
|
| 620 |
background: var(--bayan-surface-active);
|
| 621 |
}
|
| 622 |
|
| 623 |
.bayan-alt-chip--main {
|
| 624 |
+
background: var(--primary-color);
|
| 625 |
+
border-color: var(--primary-color);
|
| 626 |
color: white;
|
| 627 |
}
|
| 628 |
.bayan-alt-chip--main:hover {
|
|
|
|
| 643 |
.bayan-timing {
|
| 644 |
padding: 8px 0;
|
| 645 |
margin-top: 8px;
|
| 646 |
+
border-top: 1px solid var(--color-border);
|
| 647 |
font-size: 10px;
|
| 648 |
color: var(--bayan-text-muted);
|
| 649 |
text-align: center;
|
|
|
|
| 655 |
.bayan-summary-meta {
|
| 656 |
margin-top: 10px;
|
| 657 |
padding: 8px 12px;
|
| 658 |
+
background: var(--color-surface-hover);
|
| 659 |
border-radius: var(--bayan-radius-sm);
|
| 660 |
font-size: 11px;
|
| 661 |
color: var(--bayan-text-muted);
|
| 662 |
}
|
| 663 |
|
| 664 |
+
/* ══════════════════════════════════════════════
|
| 665 |
+
Summary Mode Toggle (paragraph / bullets)
|
| 666 |
+
══════════════════════════════════════════════ */
|
| 667 |
+
.bayan-mode-toggle {
|
| 668 |
+
display: flex;
|
| 669 |
+
gap: 0;
|
| 670 |
+
margin-bottom: 12px;
|
| 671 |
+
border: 1px solid var(--color-border);
|
| 672 |
+
border-radius: var(--bayan-radius-sm);
|
| 673 |
+
overflow: hidden;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
.bayan-mode-btn {
|
| 677 |
+
flex: 1;
|
| 678 |
+
display: flex;
|
| 679 |
+
align-items: center;
|
| 680 |
+
justify-content: center;
|
| 681 |
+
gap: 5px;
|
| 682 |
+
padding: 7px 10px;
|
| 683 |
+
border: none;
|
| 684 |
+
background: transparent;
|
| 685 |
+
color: var(--bayan-text-muted);
|
| 686 |
+
font-family: var(--bayan-font-arabic);
|
| 687 |
+
font-size: 12px;
|
| 688 |
+
font-weight: 500;
|
| 689 |
+
cursor: pointer;
|
| 690 |
+
transition: all var(--bayan-transition);
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
.bayan-mode-btn + .bayan-mode-btn {
|
| 694 |
+
border-right: 1px solid var(--color-border);
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
.bayan-mode-btn:hover {
|
| 698 |
+
background: var(--color-surface-hover);
|
| 699 |
+
color: var(--text-color);
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
.bayan-mode-btn.active {
|
| 703 |
+
background: var(--primary-color);
|
| 704 |
+
color: white;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
/* ══════════════════════════════════════════════
|
| 708 |
+
Summary Stats
|
| 709 |
+
══════════════════════════════════════════════ */
|
| 710 |
+
.bayan-summary-stats {
|
| 711 |
+
display: flex;
|
| 712 |
+
gap: 16px;
|
| 713 |
+
margin-bottom: 10px;
|
| 714 |
+
padding: 8px 12px;
|
| 715 |
+
background: var(--color-surface-hover);
|
| 716 |
+
border-radius: var(--bayan-radius-sm);
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.bayan-summary-stat {
|
| 720 |
+
font-size: 11px;
|
| 721 |
+
color: var(--bayan-text-muted);
|
| 722 |
+
display: flex;
|
| 723 |
+
gap: 4px;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
.bayan-summary-stat-value {
|
| 727 |
+
font-weight: 700;
|
| 728 |
+
color: var(--text-secondary);
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
/* ══════════════════════════════════════════════
|
| 732 |
+
Description paragraph (dialect panel)
|
| 733 |
+
══════════════════════════════════════════════ */
|
| 734 |
+
.bayan-description {
|
| 735 |
+
font-size: 11px;
|
| 736 |
+
color: var(--bayan-text-muted);
|
| 737 |
+
margin-bottom: 8px;
|
| 738 |
+
line-height: 1.6;
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
/* ══════════════════════════════════════════════
|
| 742 |
+
Input meta row (word count + import)
|
| 743 |
+
══════════════════════════════════════════════ */
|
| 744 |
+
.bayan-input-meta-row {
|
| 745 |
+
display: flex;
|
| 746 |
+
align-items: center;
|
| 747 |
+
justify-content: space-between;
|
| 748 |
+
margin-top: 6px;
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
.bayan-sr-only {
|
| 752 |
+
position: absolute;
|
| 753 |
+
width: 1px;
|
| 754 |
+
height: 1px;
|
| 755 |
+
padding: 0;
|
| 756 |
+
margin: -1px;
|
| 757 |
+
overflow: hidden;
|
| 758 |
+
clip: rect(0,0,0,0);
|
| 759 |
+
border: 0;
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
.bayan-import-btn {
|
| 763 |
+
display: inline-flex;
|
| 764 |
+
align-items: center;
|
| 765 |
+
gap: 4px;
|
| 766 |
+
padding: 3px 10px;
|
| 767 |
+
font-size: 11px;
|
| 768 |
+
font-family: var(--bayan-font-arabic);
|
| 769 |
+
color: var(--text-secondary);
|
| 770 |
+
border: 1px solid var(--color-border);
|
| 771 |
+
border-radius: var(--bayan-radius-sm);
|
| 772 |
+
background: transparent;
|
| 773 |
+
cursor: pointer;
|
| 774 |
+
transition: all var(--bayan-transition);
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
.bayan-import-btn:hover {
|
| 778 |
+
background: var(--color-surface-hover);
|
| 779 |
+
color: var(--text-color);
|
| 780 |
+
border-color: var(--primary-color);
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
/* ══════════════════════════════════════════════
|
| 784 |
+
Result actions group (copy + export)
|
| 785 |
+
══════════════════════════════════════════════ */
|
| 786 |
+
.bayan-result-actions {
|
| 787 |
+
display: flex;
|
| 788 |
+
align-items: center;
|
| 789 |
+
gap: 2px;
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
/* ══════════════════════════════════════════════
|
| 793 |
+
Export dropdown
|
| 794 |
+
══════════════════════════════════════════════ */
|
| 795 |
+
.bayan-export-dropdown {
|
| 796 |
+
position: relative;
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
.bayan-export-menu {
|
| 800 |
+
position: absolute;
|
| 801 |
+
top: 100%;
|
| 802 |
+
left: 0;
|
| 803 |
+
min-width: 130px;
|
| 804 |
+
background: var(--color-surface);
|
| 805 |
+
border: 1px solid var(--color-border);
|
| 806 |
+
border-radius: var(--bayan-radius-sm);
|
| 807 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
|
| 808 |
+
z-index: 50;
|
| 809 |
+
overflow: hidden;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
.bayan-export-item {
|
| 813 |
+
display: block;
|
| 814 |
+
width: 100%;
|
| 815 |
+
padding: 8px 12px;
|
| 816 |
+
border: none;
|
| 817 |
+
background: transparent;
|
| 818 |
+
color: var(--text-color);
|
| 819 |
+
font-family: var(--bayan-font-arabic);
|
| 820 |
+
font-size: 12px;
|
| 821 |
+
text-align: right;
|
| 822 |
+
cursor: pointer;
|
| 823 |
+
transition: background var(--bayan-transition);
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
.bayan-export-item:hover {
|
| 827 |
+
background: var(--color-surface-hover);
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
/* ══════════════════════════════════════════════
|
| 831 |
Length Selector
|
| 832 |
══════════════════════════════════════════════ */
|
|
|
|
| 844 |
align-items: center;
|
| 845 |
gap: 4px;
|
| 846 |
font-size: 12px;
|
| 847 |
+
color: var(--text-secondary);
|
| 848 |
cursor: pointer;
|
| 849 |
}
|
| 850 |
|
| 851 |
.bayan-radio input[type="radio"] {
|
| 852 |
+
accent-color: var(--primary-color);
|
| 853 |
width: 14px;
|
| 854 |
height: 14px;
|
| 855 |
cursor: pointer;
|
|
|
|
| 866 |
align-items: center;
|
| 867 |
justify-content: center;
|
| 868 |
gap: 14px;
|
| 869 |
+
background: var(--color-overlay, rgba(15, 15, 20, 0.85));
|
| 870 |
backdrop-filter: blur(4px);
|
| 871 |
z-index: 100;
|
| 872 |
}
|
|
|
|
| 874 |
.bayan-spinner {
|
| 875 |
width: 36px;
|
| 876 |
height: 36px;
|
| 877 |
+
border: 3px solid var(--color-border);
|
| 878 |
+
border-top-color: var(--primary-color);
|
| 879 |
border-radius: 50%;
|
| 880 |
animation: spin 0.8s linear infinite;
|
| 881 |
}
|
|
|
|
| 886 |
|
| 887 |
.bayan-loading-text {
|
| 888 |
font-size: 13px;
|
| 889 |
+
color: var(--text-secondary);
|
| 890 |
}
|
| 891 |
|
| 892 |
/* ══════════════════════════════════════════════
|
|
|
|
| 898 |
left: 50%;
|
| 899 |
transform: translateX(-50%);
|
| 900 |
padding: 8px 18px;
|
| 901 |
+
background: var(--color-surface);
|
| 902 |
+
border: 1px solid var(--color-border);
|
| 903 |
border-radius: 20px;
|
| 904 |
+
color: var(--text-color);
|
| 905 |
font-size: 12px;
|
| 906 |
font-family: var(--bayan-font-arabic);
|
| 907 |
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
|
|
|
| 940 |
white-space: nowrap;
|
| 941 |
}
|
| 942 |
|
| 943 |
+
/* ══════════════════════════════════════════════
|
| 944 |
+
Quran Result
|
| 945 |
+
══════════════════════════════════════════════ */
|
| 946 |
+
.bayan-quran-result {
|
| 947 |
+
background: var(--color-surface);
|
| 948 |
+
border: 1px solid rgba(6, 182, 212, 0.2);
|
| 949 |
+
border-radius: var(--bayan-radius);
|
| 950 |
+
padding: 14px;
|
| 951 |
+
animation: fadeSlideIn 300ms ease-out;
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
.bayan-quran-result-header {
|
| 955 |
+
display: flex;
|
| 956 |
+
align-items: center;
|
| 957 |
+
justify-content: space-between;
|
| 958 |
+
margin-bottom: 10px;
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
.bayan-quran-uthmani {
|
| 962 |
+
font-family: 'Amiri Quran', 'Cairo', serif;
|
| 963 |
+
font-size: 20px;
|
| 964 |
+
line-height: 2.2;
|
| 965 |
+
text-align: center;
|
| 966 |
+
color: var(--text-color);
|
| 967 |
+
margin-bottom: 4px;
|
| 968 |
+
}
|
| 969 |
+
|
| 970 |
+
.bayan-quran-reference {
|
| 971 |
+
text-align: center;
|
| 972 |
+
font-size: 12px;
|
| 973 |
+
font-weight: 600;
|
| 974 |
+
color: #06b6d4;
|
| 975 |
+
margin-bottom: 8px;
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
.bayan-quran-translate {
|
| 979 |
+
border-top: 1px solid var(--color-border);
|
| 980 |
+
padding-top: 10px;
|
| 981 |
+
margin-top: 10px;
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
.bayan-quran-translate-row {
|
| 985 |
+
display: flex;
|
| 986 |
+
align-items: center;
|
| 987 |
+
gap: 6px;
|
| 988 |
+
flex-wrap: wrap;
|
| 989 |
+
margin-bottom: 8px;
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
.bayan-quran-lang-select {
|
| 993 |
+
flex: 1;
|
| 994 |
+
min-width: 120px;
|
| 995 |
+
padding: 5px 8px;
|
| 996 |
+
font-size: 12px;
|
| 997 |
+
color: var(--text-color);
|
| 998 |
+
background: var(--color-surface);
|
| 999 |
+
border: 1px solid var(--color-border);
|
| 1000 |
+
border-radius: var(--bayan-radius-sm);
|
| 1001 |
+
font-family: var(--bayan-font-arabic);
|
| 1002 |
+
cursor: pointer;
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
.bayan-quran-lang-select:focus {
|
| 1006 |
+
outline: none;
|
| 1007 |
+
border-color: #06b6d4;
|
| 1008 |
+
}
|
| 1009 |
+
|
| 1010 |
+
.bayan-quran-translation {
|
| 1011 |
+
padding: 10px;
|
| 1012 |
+
border-radius: var(--bayan-radius-sm);
|
| 1013 |
+
background: rgba(6, 182, 212, 0.06);
|
| 1014 |
+
border: 1px solid rgba(6, 182, 212, 0.15);
|
| 1015 |
+
}
|
| 1016 |
+
|
| 1017 |
+
.bayan-quran-translation p {
|
| 1018 |
+
font-size: 16px;
|
| 1019 |
+
line-height: 2;
|
| 1020 |
+
text-align: center;
|
| 1021 |
+
color: var(--text-color);
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
/* ══════════════════════════════════════════════
|
| 1025 |
Utilities
|
| 1026 |
══════════════════════════════════════════════ */
|
|
|
|
| 1029 |
}
|
| 1030 |
|
| 1031 |
|
| 1032 |
+
/* Light theme handled by tokens.css */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1033 |
|
| 1034 |
/* Theme Toggle Button Styles */
|
| 1035 |
.theme-toggle-animated {
|
|
|
|
| 1040 |
height: 32px;
|
| 1041 |
border: none;
|
| 1042 |
border-radius: 50%;
|
| 1043 |
+
background: var(--color-surface-hover);
|
| 1044 |
+
color: var(--text-secondary);
|
| 1045 |
cursor: pointer;
|
| 1046 |
transition: background 0.3s ease, transform 0.3s ease, color 0.3s ease;
|
| 1047 |
position: relative;
|
|
|
|
| 1050 |
}
|
| 1051 |
|
| 1052 |
.theme-toggle-animated:hover {
|
| 1053 |
+
background: var(--primary-color);
|
| 1054 |
color: #fff;
|
| 1055 |
transform: rotate(15deg);
|
| 1056 |
}
|
extension/popup.html
CHANGED
|
@@ -3,7 +3,11 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=400">
|
|
|
|
|
|
|
|
|
|
| 6 |
<title>بيان</title>
|
|
|
|
| 7 |
<link rel="stylesheet" href="popup.css">
|
| 8 |
</head>
|
| 9 |
<body>
|
|
@@ -11,12 +15,12 @@
|
|
| 11 |
<!-- Header -->
|
| 12 |
<!-- ══════════════════════════════════════════════ -->
|
| 13 |
<header class="bayan-header">
|
| 14 |
-
<
|
| 15 |
<img src="assets/icons/icon48.png" alt="بيان" width="28" height="28" style="border-radius:6px;">
|
| 16 |
-
<div style="height:24px; width:2px; background-color:#d1d5db; border-radius:9999px; flex-shrink:0;"></div>
|
| 17 |
<span class="bayan-header-title">بيان</span>
|
| 18 |
-
</
|
| 19 |
-
|
| 20 |
<div style="display: flex; align-items: center; gap: 12px;">
|
| 21 |
<button id="ext-theme-toggle" class="theme-toggle-animated" aria-label="تبديل السمة" type="button">
|
| 22 |
<svg class="theme-icon-sun" width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
|
@@ -26,6 +30,9 @@
|
|
| 26 |
<span class="bayan-status-dot"></span>
|
| 27 |
<span class="bayan-status-text" id="status-text">جاهز</span>
|
| 28 |
</div>
|
|
|
|
|
|
|
|
|
|
| 29 |
</div>
|
| 30 |
</header>
|
| 31 |
|
|
@@ -49,10 +56,6 @@
|
|
| 49 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
| 50 |
تدقيق النص القرآني
|
| 51 |
</button>
|
| 52 |
-
<button class="bayan-tab" role="tab" aria-selected="false" data-tab="autocomplete" id="tab-autocomplete">
|
| 53 |
-
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
| 54 |
-
إكمال
|
| 55 |
-
</button>
|
| 56 |
</nav>
|
| 57 |
|
| 58 |
<!-- ══════════════════════════════════════════════ -->
|
|
@@ -61,7 +64,12 @@
|
|
| 61 |
<section class="bayan-panel active" id="panel-correct" role="tabpanel">
|
| 62 |
<!-- Input area -->
|
| 63 |
<div class="bayan-input-group">
|
| 64 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
<textarea
|
| 66 |
id="input-text"
|
| 67 |
class="bayan-textarea"
|
|
@@ -86,47 +94,41 @@
|
|
| 86 |
<button class="bayan-btn bayan-btn-ghost" id="btn-clear" type="button">مسح</button>
|
| 87 |
</div>
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
<!-- Score ring -->
|
| 90 |
<div class="bayan-score-section is-hidden" id="score-section">
|
|
|
|
| 91 |
<div class="bayan-score-ring">
|
| 92 |
-
<svg viewBox="0 0 160 160" width="
|
| 93 |
-
<
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
stroke-dasharray="440" stroke-dashoffset="440" stroke-linecap="round"
|
| 96 |
transform="rotate(-90 80 80)" id="score-circle"/>
|
| 97 |
</svg>
|
| 98 |
<span class="bayan-score-value" id="score-value">--</span>
|
| 99 |
</div>
|
| 100 |
-
<
|
| 101 |
-
|
| 102 |
-
<
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
<span class="bayan-count bayan-count-punctuation"><span id="count-punctuation">٠</span> ترقيم</span>
|
| 106 |
-
</div>
|
| 107 |
</div>
|
| 108 |
</div>
|
| 109 |
|
| 110 |
-
<!-- Corrected text result -->
|
| 111 |
-
<div class="bayan-result is-hidden" id="result-section">
|
| 112 |
-
<div class="bayan-result-header">
|
| 113 |
-
<span class="bayan-result-title">النص المصحّح</span>
|
| 114 |
-
<button class="bayan-btn-icon" id="btn-copy-result" type="button" title="نسخ">
|
| 115 |
-
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 116 |
-
</button>
|
| 117 |
-
</div>
|
| 118 |
-
<div class="bayan-result-text" id="result-text" dir="rtl"></div>
|
| 119 |
-
</div>
|
| 120 |
-
|
| 121 |
-
<!-- Suggestions list -->
|
| 122 |
-
<div class="bayan-suggestions is-hidden" id="suggestions-section">
|
| 123 |
-
<div class="bayan-suggestions-header">
|
| 124 |
-
<span class="bayan-suggestions-title">الاقتراحات</span>
|
| 125 |
-
<button class="bayan-btn bayan-btn-sm" id="btn-apply-all" type="button">تطبيق الكل</button>
|
| 126 |
-
</div>
|
| 127 |
-
<div class="bayan-suggestions-list" id="suggestions-list" role="list"></div>
|
| 128 |
-
</div>
|
| 129 |
-
|
| 130 |
<!-- Timing info -->
|
| 131 |
<div class="bayan-timing is-hidden" id="timing-section">
|
| 132 |
<span id="timing-text"></span>
|
|
@@ -148,11 +150,28 @@
|
|
| 148 |
rows="5"
|
| 149 |
maxlength="5000"
|
| 150 |
></textarea>
|
| 151 |
-
<div class="bayan-input-meta">
|
| 152 |
-
<span id="summary-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
</div>
|
| 154 |
</div>
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
<!-- Length selector -->
|
| 157 |
<div class="bayan-length-selector">
|
| 158 |
<label class="bayan-label">طول الملخص</label>
|
|
@@ -173,14 +192,30 @@
|
|
| 173 |
|
| 174 |
<!-- Summary result -->
|
| 175 |
<div class="bayan-result is-hidden" id="summary-result-section">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
<div class="bayan-result-header">
|
| 177 |
<span class="bayan-result-title">الملخص</span>
|
| 178 |
-
<
|
| 179 |
-
<
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
</div>
|
| 182 |
<div class="bayan-result-text" id="summary-text" dir="rtl"></div>
|
| 183 |
-
<div class="bayan-summary-meta" id="summary-meta"></div>
|
| 184 |
</div>
|
| 185 |
</section>
|
| 186 |
|
|
@@ -189,12 +224,13 @@
|
|
| 189 |
<!-- ══════════════════════════════════════════════ -->
|
| 190 |
<section class="bayan-panel" id="panel-dialect" role="tabpanel">
|
| 191 |
<div class="bayan-input-group">
|
| 192 |
-
<label class="bayan-label" for="dialect-input-text">حو
|
|
|
|
| 193 |
<textarea
|
| 194 |
id="dialect-input-text"
|
| 195 |
class="bayan-textarea"
|
| 196 |
dir="rtl"
|
| 197 |
-
placeholder="
|
| 198 |
rows="5"
|
| 199 |
maxlength="5000"
|
| 200 |
></textarea>
|
|
@@ -208,7 +244,7 @@
|
|
| 208 |
</div>
|
| 209 |
<div class="bayan-result is-hidden" id="dialect-result-section">
|
| 210 |
<div class="bayan-result-header">
|
| 211 |
-
<span class="bayan-result-title">النص بالفصحى</span>
|
| 212 |
<button class="bayan-btn-icon" id="btn-copy-dialect" type="button" title="نسخ">
|
| 213 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 214 |
</button>
|
|
@@ -236,48 +272,53 @@
|
|
| 236 |
<div class="bayan-actions">
|
| 237 |
<button class="bayan-btn bayan-btn-primary" id="btn-quran" type="button">
|
| 238 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
| 239 |
-
تدقيق
|
| 240 |
</button>
|
| 241 |
</div>
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
| 245 |
<button class="bayan-btn-icon" id="btn-copy-quran" type="button" title="نسخ">
|
| 246 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 247 |
</button>
|
| 248 |
</div>
|
| 249 |
-
<
|
| 250 |
-
<
|
| 251 |
-
</div>
|
| 252 |
-
</section>
|
| 253 |
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
</div>
|
| 280 |
-
<div class="bayan-suggestions-list" id="autocomplete-list" role="list"></div>
|
| 281 |
</div>
|
| 282 |
</section>
|
| 283 |
|
|
@@ -294,6 +335,10 @@
|
|
| 294 |
<!-- ══════════════════════════════════════════════ -->
|
| 295 |
<div class="bayan-toast" id="toast" role="status" aria-live="polite"></div>
|
| 296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
<!-- Scripts: constants first, then config, then shared modules, then popup logic -->
|
| 298 |
<script src="shared/constants.js"></script>
|
| 299 |
<script src="shared/config.js"></script>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=400">
|
| 6 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 7 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
<title>بيان</title>
|
| 10 |
+
<link rel="stylesheet" href="shared/css/tokens.css">
|
| 11 |
<link rel="stylesheet" href="popup.css">
|
| 12 |
</head>
|
| 13 |
<body>
|
|
|
|
| 15 |
<!-- Header -->
|
| 16 |
<!-- ══════════════════════════════════════════════ -->
|
| 17 |
<header class="bayan-header">
|
| 18 |
+
<a href="https://bayan10-bayan-api.hf.space/" target="_blank" rel="noopener noreferrer" class="bayan-header-brand" style="text-decoration:none;color:inherit;">
|
| 19 |
<img src="assets/icons/icon48.png" alt="بيان" width="28" height="28" style="border-radius:6px;">
|
| 20 |
+
<div style="height:24px; width:2px; background-color:var(--color-border-strong, #d1d5db); border-radius:9999px; flex-shrink:0;"></div>
|
| 21 |
<span class="bayan-header-title">بيان</span>
|
| 22 |
+
</a>
|
| 23 |
+
|
| 24 |
<div style="display: flex; align-items: center; gap: 12px;">
|
| 25 |
<button id="ext-theme-toggle" class="theme-toggle-animated" aria-label="تبديل السمة" type="button">
|
| 26 |
<svg class="theme-icon-sun" width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
|
|
|
| 30 |
<span class="bayan-status-dot"></span>
|
| 31 |
<span class="bayan-status-text" id="status-text">جاهز</span>
|
| 32 |
</div>
|
| 33 |
+
<button id="btn-close-popup" class="bayan-btn-icon" type="button" title="إغلاق" style="margin-right:4px;">
|
| 34 |
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
| 35 |
+
</button>
|
| 36 |
</div>
|
| 37 |
</header>
|
| 38 |
|
|
|
|
| 56 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
| 57 |
تدقيق النص القرآني
|
| 58 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
</nav>
|
| 60 |
|
| 61 |
<!-- ══════════════════════════════════════════════ -->
|
|
|
|
| 64 |
<section class="bayan-panel active" id="panel-correct" role="tabpanel">
|
| 65 |
<!-- Input area -->
|
| 66 |
<div class="bayan-input-group">
|
| 67 |
+
<div class="bayan-label-row">
|
| 68 |
+
<label class="bayan-label" for="input-text">أدخل النص العربي</label>
|
| 69 |
+
<button class="bayan-btn-icon" id="btn-copy-text" type="button" title="نسخ النص">
|
| 70 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 71 |
+
</button>
|
| 72 |
+
</div>
|
| 73 |
<textarea
|
| 74 |
id="input-text"
|
| 75 |
class="bayan-textarea"
|
|
|
|
| 94 |
<button class="bayan-btn bayan-btn-ghost" id="btn-clear" type="button">مسح</button>
|
| 95 |
</div>
|
| 96 |
|
| 97 |
+
<!-- Suggestions list (above score) -->
|
| 98 |
+
<div class="bayan-suggestions is-hidden" id="suggestions-section">
|
| 99 |
+
<div class="bayan-suggestions-header">
|
| 100 |
+
<span class="bayan-suggestions-title">الاقتراحات</span>
|
| 101 |
+
<button class="bayan-btn bayan-btn-sm" id="btn-apply-all" type="button">تطبيق الكل</button>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="bayan-suggestions-list" id="suggestions-list" role="list"></div>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
<!-- Score ring -->
|
| 107 |
<div class="bayan-score-section is-hidden" id="score-section">
|
| 108 |
+
<span class="bayan-score-title">تقييم الكتابة</span>
|
| 109 |
<div class="bayan-score-ring">
|
| 110 |
+
<svg viewBox="0 0 160 160" width="120" height="120">
|
| 111 |
+
<defs>
|
| 112 |
+
<linearGradient id="scoreGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
| 113 |
+
<stop offset="0%" stop-color="var(--color-primary, #6BA3E0)"/>
|
| 114 |
+
<stop offset="100%" stop-color="var(--color-secondary, #A594E8)"/>
|
| 115 |
+
</linearGradient>
|
| 116 |
+
</defs>
|
| 117 |
+
<circle cx="80" cy="80" r="70" fill="none" stroke="var(--color-border)" stroke-width="10"/>
|
| 118 |
+
<circle cx="80" cy="80" r="70" fill="none" stroke="url(#scoreGradient)" stroke-width="10"
|
| 119 |
stroke-dasharray="440" stroke-dashoffset="440" stroke-linecap="round"
|
| 120 |
transform="rotate(-90 80 80)" id="score-circle"/>
|
| 121 |
</svg>
|
| 122 |
<span class="bayan-score-value" id="score-value">--</span>
|
| 123 |
</div>
|
| 124 |
+
<span class="bayan-score-hint" id="score-hint">ابدأ الكتابة لرؤية تقييمك</span>
|
| 125 |
+
<div class="bayan-counts" id="error-counts">
|
| 126 |
+
<span class="bayan-count bayan-count-spelling"><span id="count-spelling">٠</span> إملائي</span>
|
| 127 |
+
<span class="bayan-count bayan-count-grammar"><span id="count-grammar">٠</span> نحوي</span>
|
| 128 |
+
<span class="bayan-count bayan-count-punctuation"><span id="count-punctuation">٠</span> ترقيم</span>
|
|
|
|
|
|
|
| 129 |
</div>
|
| 130 |
</div>
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
<!-- Timing info -->
|
| 133 |
<div class="bayan-timing is-hidden" id="timing-section">
|
| 134 |
<span id="timing-text"></span>
|
|
|
|
| 150 |
rows="5"
|
| 151 |
maxlength="5000"
|
| 152 |
></textarea>
|
| 153 |
+
<div class="bayan-input-meta-row">
|
| 154 |
+
<span class="bayan-input-meta"><span id="summary-word-count-input">٠</span> كلمة</span>
|
| 155 |
+
<label class="bayan-import-btn" title="استيراد ملف">
|
| 156 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
|
| 157 |
+
استيراد ملف
|
| 158 |
+
<input type="file" id="summary-import-input" class="bayan-sr-only" accept=".txt,.docx">
|
| 159 |
+
</label>
|
| 160 |
</div>
|
| 161 |
</div>
|
| 162 |
|
| 163 |
+
<!-- Mode toggle (paragraph / bullets) -->
|
| 164 |
+
<div class="bayan-mode-toggle">
|
| 165 |
+
<button type="button" class="bayan-mode-btn active" id="summary-mode-paragraph" data-mode="paragraph">
|
| 166 |
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 24 24"><path d="M3 5h18v2H3V5zm0 8h18v2H3v-2zm0 4h12v2H3v-2z"/></svg>
|
| 167 |
+
فقرة
|
| 168 |
+
</button>
|
| 169 |
+
<button type="button" class="bayan-mode-btn" id="summary-mode-bullets" data-mode="bullets">
|
| 170 |
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 24 24"><path d="M4 6h2v2H4V6zm4 0h12v2H8V6zM4 11h2v2H4v-2zm4 0h12v2H8v-2zm-4 5h2v2H4v-2zm4 0h12v2H8v-2z"/></svg>
|
| 171 |
+
نقاط
|
| 172 |
+
</button>
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
<!-- Length selector -->
|
| 176 |
<div class="bayan-length-selector">
|
| 177 |
<label class="bayan-label">طول الملخص</label>
|
|
|
|
| 192 |
|
| 193 |
<!-- Summary result -->
|
| 194 |
<div class="bayan-result is-hidden" id="summary-result-section">
|
| 195 |
+
<!-- Summary stats -->
|
| 196 |
+
<div class="bayan-summary-stats is-hidden" id="summary-stats">
|
| 197 |
+
<div class="bayan-summary-stat"><span>كلمات الملخص:</span> <span class="bayan-summary-stat-value" id="summary-word-count">٠</span></div>
|
| 198 |
+
<div class="bayan-summary-stat"><span>نسبة الاختصار:</span> <span class="bayan-summary-stat-value" id="summary-compression">٠٪</span></div>
|
| 199 |
+
</div>
|
| 200 |
<div class="bayan-result-header">
|
| 201 |
<span class="bayan-result-title">الملخص</span>
|
| 202 |
+
<div class="bayan-result-actions">
|
| 203 |
+
<button class="bayan-btn-icon" id="btn-copy-summary" type="button" title="نسخ">
|
| 204 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 205 |
+
</button>
|
| 206 |
+
<div class="bayan-export-dropdown" id="summary-export-wrap">
|
| 207 |
+
<button class="bayan-btn-icon" id="btn-export-summary" type="button" title="تصدير">
|
| 208 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M12 4v12m0 0l-4-4m4 4l4-4"/></svg>
|
| 209 |
+
</button>
|
| 210 |
+
<div class="bayan-export-menu is-hidden" id="summary-export-menu">
|
| 211 |
+
<button type="button" class="bayan-export-item" data-format="txt">نصي (.txt)</button>
|
| 212 |
+
<button type="button" class="bayan-export-item" data-format="docx">Word (.docx)</button>
|
| 213 |
+
<button type="button" class="bayan-export-item" data-format="pdf">PDF (.pdf)</button>
|
| 214 |
+
</div>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
</div>
|
| 218 |
<div class="bayan-result-text" id="summary-text" dir="rtl"></div>
|
|
|
|
| 219 |
</div>
|
| 220 |
</section>
|
| 221 |
|
|
|
|
| 224 |
<!-- ══════════════════════════════════════════════ -->
|
| 225 |
<section class="bayan-panel" id="panel-dialect" role="tabpanel">
|
| 226 |
<div class="bayan-input-group">
|
| 227 |
+
<label class="bayan-label" for="dialect-input-text">تحويل اللهجات للفصحى</label>
|
| 228 |
+
<p class="bayan-description">اكتب نصًا بأي لهجة عربية (مصرية، خليجية، شامية...) وسنحوّله إلى لغة عربية فصحى سليمة.</p>
|
| 229 |
<textarea
|
| 230 |
id="dialect-input-text"
|
| 231 |
class="bayan-textarea"
|
| 232 |
dir="rtl"
|
| 233 |
+
placeholder="اكتب النص باللهجة هنا..."
|
| 234 |
rows="5"
|
| 235 |
maxlength="5000"
|
| 236 |
></textarea>
|
|
|
|
| 244 |
</div>
|
| 245 |
<div class="bayan-result is-hidden" id="dialect-result-section">
|
| 246 |
<div class="bayan-result-header">
|
| 247 |
+
<span class="bayan-result-title" style="color: var(--color-primary, #6BA3E0);">✓ النص بالعربية الفصحى</span>
|
| 248 |
<button class="bayan-btn-icon" id="btn-copy-dialect" type="button" title="نسخ">
|
| 249 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 250 |
</button>
|
|
|
|
| 272 |
<div class="bayan-actions">
|
| 273 |
<button class="bayan-btn bayan-btn-primary" id="btn-quran" type="button">
|
| 274 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
| 275 |
+
بحث وتدقيق
|
| 276 |
</button>
|
| 277 |
</div>
|
| 278 |
+
|
| 279 |
+
<!-- Quran result -->
|
| 280 |
+
<div class="bayan-quran-result is-hidden" id="quran-result-section">
|
| 281 |
+
<div class="bayan-quran-result-header">
|
| 282 |
+
<span style="color:#06b6d4; font-size:12px; font-weight:700;">✓ النص القرآني المدقق</span>
|
| 283 |
<button class="bayan-btn-icon" id="btn-copy-quran" type="button" title="نسخ">
|
| 284 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 285 |
</button>
|
| 286 |
</div>
|
| 287 |
+
<p id="quran-uthmani" class="bayan-quran-uthmani" dir="rtl"></p>
|
| 288 |
+
<p id="quran-reference" class="bayan-quran-reference"></p>
|
|
|
|
|
|
|
| 289 |
|
| 290 |
+
<!-- Translation -->
|
| 291 |
+
<div class="bayan-quran-translate">
|
| 292 |
+
<div class="bayan-quran-translate-row">
|
| 293 |
+
<svg width="14" height="14" fill="none" stroke="#06b6d4" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"/></svg>
|
| 294 |
+
<span style="font-size:12px; font-weight:600; color:var(--text-secondary);">ترجمة الآية</span>
|
| 295 |
+
<select id="quran-lang-select" class="bayan-quran-lang-select">
|
| 296 |
+
<option value="">— اختر لغة —</option>
|
| 297 |
+
<option value="english">🇬🇧 English</option>
|
| 298 |
+
<option value="french">🇫🇷 Français</option>
|
| 299 |
+
<option value="turkish">🇹🇷 Türkçe</option>
|
| 300 |
+
<option value="persian">🇮🇷 فارسی</option>
|
| 301 |
+
<option value="russian">🇷🇺 Русский</option>
|
| 302 |
+
<option value="spanish">🇪🇸 Español</option>
|
| 303 |
+
<option value="german">🇩🇪 Deutsch</option>
|
| 304 |
+
<option value="indonesian">🇮🇩 Indonesia</option>
|
| 305 |
+
<option value="malay">🇲🇾 Melayu</option>
|
| 306 |
+
<option value="bengali">🇧🇩 বাংলা</option>
|
| 307 |
+
<option value="bosnian">🇧🇦 Bosanski</option>
|
| 308 |
+
<option value="portuguese">🇵🇹 Português</option>
|
| 309 |
+
<option value="uzbek">🇺🇿 O'zbek</option>
|
| 310 |
+
</select>
|
| 311 |
+
</div>
|
| 312 |
+
<div id="quran-translation-section" class="bayan-quran-translation is-hidden">
|
| 313 |
+
<p id="quran-trans-text" dir="auto"></p>
|
| 314 |
+
<p id="quran-trans-ref" class="bayan-quran-reference" style="display:none;"></p>
|
| 315 |
+
<div class="bayan-result-actions" id="quran-trans-actions" style="display:none; justify-content:flex-end; gap:4px; margin-top:8px;">
|
| 316 |
+
<button class="bayan-btn-icon" id="btn-copy-quran-trans" type="button" title="نسخ الترجمة">
|
| 317 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 318 |
+
</button>
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
</div>
|
|
|
|
| 322 |
</div>
|
| 323 |
</section>
|
| 324 |
|
|
|
|
| 335 |
<!-- ══════════════════════════════════════════════ -->
|
| 336 |
<div class="bayan-toast" id="toast" role="status" aria-live="polite"></div>
|
| 337 |
|
| 338 |
+
<!-- Vendor libs for import/export -->
|
| 339 |
+
<script src="shared/vendor/mammoth.browser.min.js"></script>
|
| 340 |
+
<script src="shared/vendor/docx.umd.js"></script>
|
| 341 |
+
<script src="shared/vendor/html2pdf.bundle.min.js"></script>
|
| 342 |
<!-- Scripts: constants first, then config, then shared modules, then popup logic -->
|
| 343 |
<script src="shared/constants.js"></script>
|
| 344 |
<script src="shared/config.js"></script>
|
extension/popup.js
CHANGED
|
@@ -15,10 +15,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 15 |
const btnCorrect = document.getElementById('btn-correct');
|
| 16 |
const btnClear = document.getElementById('btn-clear');
|
| 17 |
const btnApplyAll = document.getElementById('btn-apply-all');
|
| 18 |
-
const
|
| 19 |
const scoreSection = document.getElementById('score-section');
|
| 20 |
-
const resultSection = document.getElementById('result-section');
|
| 21 |
-
const resultText = document.getElementById('result-text');
|
| 22 |
const suggestionsSection = document.getElementById('suggestions-section');
|
| 23 |
const suggestionsList = document.getElementById('suggestions-list');
|
| 24 |
const timingSection = document.getElementById('timing-section');
|
|
@@ -28,11 +26,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 28 |
|
| 29 |
// Summary tab elements
|
| 30 |
const summaryInputText = document.getElementById('summary-input-text');
|
| 31 |
-
const
|
| 32 |
const btnSummarize = document.getElementById('btn-summarize');
|
| 33 |
const summaryResultSection = document.getElementById('summary-result-section');
|
| 34 |
const summaryText = document.getElementById('summary-text');
|
| 35 |
-
const
|
|
|
|
|
|
|
| 36 |
const btnCopySummary = document.getElementById('btn-copy-summary');
|
| 37 |
|
| 38 |
// Score elements
|
|
@@ -64,6 +64,54 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 64 |
|
| 65 |
const SCORE_CIRCUMFERENCE = 440;
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
// ══════════════════════════════════════════════════════════
|
| 68 |
// Tab switching
|
| 69 |
// ══════════════════════════════════════════════════════════
|
|
@@ -77,6 +125,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 77 |
document.querySelectorAll('.bayan-panel').forEach((p) => {
|
| 78 |
p.classList.toggle('active', p.id === `panel-${targetTab}`);
|
| 79 |
});
|
|
|
|
| 80 |
});
|
| 81 |
});
|
| 82 |
|
|
@@ -93,6 +142,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 93 |
|
| 94 |
inputText.addEventListener('input', () => {
|
| 95 |
updateCounts(inputText, charCount, wordCount);
|
|
|
|
| 96 |
|
| 97 |
// MED-2: Detect user edit after analysis → mark stale
|
| 98 |
if (currentSuggestions.length > 0 && inputText.value !== analyzedText) {
|
|
@@ -100,7 +150,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 100 |
}
|
| 101 |
});
|
| 102 |
|
| 103 |
-
summaryInputText.addEventListener('input', () =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
// ══════════════════════════════════════════════════════════
|
| 106 |
// MED-2: Staleness management
|
|
@@ -111,17 +166,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 111 |
* Disables suggestion actions and shows a re-analysis prompt.
|
| 112 |
*/
|
| 113 |
function markStale() {
|
| 114 |
-
if (isStale) return;
|
| 115 |
isStale = true;
|
| 116 |
|
| 117 |
-
// Visual indicator: dim the results area
|
| 118 |
-
if (resultSection) resultSection.classList.add('bayan-stale');
|
| 119 |
if (suggestionsSection) suggestionsSection.classList.add('bayan-stale');
|
| 120 |
|
| 121 |
-
// Show re-analysis toast
|
| 122 |
showToast('⚠ النص تغيّر — أعد التحليل لتحديث الاقتراحات', 4000);
|
| 123 |
|
| 124 |
-
// Update button text to indicate re-analysis needed
|
| 125 |
btnCorrect.innerHTML = `
|
| 126 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h5M20 20v-5h-5M4 9a8 8 0 0114-3M20 15a8 8 0 01-14 3"/></svg>
|
| 127 |
إعادة التحليل`;
|
|
@@ -132,10 +183,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 132 |
*/
|
| 133 |
function clearStale() {
|
| 134 |
isStale = false;
|
| 135 |
-
if (resultSection) resultSection.classList.remove('bayan-stale');
|
| 136 |
if (suggestionsSection) suggestionsSection.classList.remove('bayan-stale');
|
| 137 |
|
| 138 |
-
// Restore button text
|
| 139 |
btnCorrect.innerHTML = `
|
| 140 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4"/></svg>
|
| 141 |
تحليل وتصحيح`;
|
|
@@ -236,10 +285,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 236 |
updateScore(counts.spelling, counts.grammar, counts.punctuation);
|
| 237 |
renderSuggestions(currentSuggestions);
|
| 238 |
|
| 239 |
-
// Re-render highlighted text with updated offsets
|
| 240 |
-
resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
|
| 241 |
-
|
| 242 |
showToast('✓ تم التصحيح');
|
|
|
|
| 243 |
});
|
| 244 |
});
|
| 245 |
|
|
@@ -255,11 +302,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 255 |
analyzedText = '';
|
| 256 |
updateCounts(inputText, charCount, wordCount);
|
| 257 |
scoreSection.classList.add('is-hidden');
|
| 258 |
-
resultSection.classList.add('is-hidden');
|
| 259 |
suggestionsSection.classList.add('is-hidden');
|
| 260 |
timingSection.classList.add('is-hidden');
|
| 261 |
currentSuggestions = [];
|
| 262 |
clearStale();
|
|
|
|
| 263 |
});
|
| 264 |
|
| 265 |
// ══════════════════════════════════════════════════════════
|
|
@@ -271,8 +318,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 271 |
showToast('أدخل نصاً للتحليل');
|
| 272 |
return;
|
| 273 |
}
|
| 274 |
-
if (text.length <
|
| 275 |
-
showToast('
|
| 276 |
return;
|
| 277 |
}
|
| 278 |
if (text.length > CONFIG.MAX_ANALYZE_LENGTH) {
|
|
@@ -296,9 +343,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 296 |
inputText.value = analyzedText;
|
| 297 |
updateCounts(inputText, charCount, wordCount);
|
| 298 |
|
| 299 |
-
// Show corrected text
|
| 300 |
-
|
| 301 |
-
resultText.innerHTML = renderHighlightedText(analyzedText, suggestions);
|
| 302 |
|
| 303 |
// Update score
|
| 304 |
const counts = countByType(suggestions);
|
|
@@ -316,6 +362,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 316 |
if (suggestions.length === 0) {
|
| 317 |
showToast('نصك ممتاز! لم نجد أي أخطاء ✨');
|
| 318 |
}
|
|
|
|
|
|
|
| 319 |
} else {
|
| 320 |
showToast('تعذّر التحليل — حاول مرة أخرى');
|
| 321 |
}
|
|
@@ -344,22 +392,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 344 |
inputText.value = analyzedText;
|
| 345 |
updateCounts(inputText, charCount, wordCount);
|
| 346 |
currentSuggestions = [];
|
| 347 |
-
resultText.innerHTML = escapeHtml(analyzedText);
|
| 348 |
updateScore(0, 0, 0);
|
| 349 |
renderSuggestions([]);
|
| 350 |
showToast('✓ تم تطبيق جميع التصحيحات');
|
|
|
|
| 351 |
});
|
| 352 |
|
| 353 |
// ══════════════════════════════════════════════════════════
|
| 354 |
-
// Copy
|
| 355 |
// ══════════════════════════════════════════════════════════
|
| 356 |
-
|
| 357 |
-
const text =
|
| 358 |
navigator.clipboard.writeText(text)
|
| 359 |
.then(() => showToast('✓ تم نسخ النص'))
|
| 360 |
.catch(() => showToast('تعذّر النسخ'));
|
| 361 |
});
|
| 362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
// ══════════════════════════════════════════════════════════
|
| 364 |
// Summarize button
|
| 365 |
// ══════════════════════════════════════════════════════════
|
|
@@ -383,8 +445,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 383 |
|
| 384 |
if (data.status === 'success' && data.summary) {
|
| 385 |
summaryResultSection.classList.remove('is-hidden');
|
| 386 |
-
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
showToast('✓ تم التلخيص');
|
| 389 |
} else {
|
| 390 |
showToast('تعذّر التلخيص — حاول مرة أخرى');
|
|
@@ -401,7 +490,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 401 |
// Copy summary (MED-1: .catch() for clipboard errors)
|
| 402 |
// ══════════════════════════════════════════════════════════
|
| 403 |
btnCopySummary.addEventListener('click', () => {
|
| 404 |
-
const text = summaryText.textContent || '';
|
| 405 |
navigator.clipboard.writeText(text)
|
| 406 |
.then(() => showToast('✓ تم نسخ الملخص'))
|
| 407 |
.catch(() => showToast('تعذّر النسخ'));
|
|
@@ -418,7 +507,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 418 |
const btnCopyDialect = document.getElementById('btn-copy-dialect');
|
| 419 |
|
| 420 |
if (dialectInput) {
|
| 421 |
-
dialectInput.addEventListener('input', () =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
btnDialect.addEventListener('click', async () => {
|
| 424 |
const text = dialectInput.value.trim();
|
|
@@ -450,147 +543,257 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 450 |
}
|
| 451 |
|
| 452 |
// ═══════════════════════════════════════════════════════���══
|
| 453 |
-
// Quran verification
|
| 454 |
// ══════════════════════════════════════════════════════════
|
| 455 |
const quranInput = document.getElementById('quran-input-text');
|
| 456 |
const quranCharCount = document.getElementById('quran-char-count');
|
| 457 |
const btnQuran = document.getElementById('btn-quran');
|
| 458 |
const quranResultSection = document.getElementById('quran-result-section');
|
| 459 |
-
const
|
| 460 |
-
const
|
| 461 |
const btnCopyQuran = document.getElementById('btn-copy-quran');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
|
| 463 |
if (quranInput) {
|
| 464 |
-
quranInput.addEventListener('input', () => updateCounts(quranInput, quranCharCount, null));
|
| 465 |
|
| 466 |
btnQuran.addEventListener('click', async () => {
|
| 467 |
const text = quranInput.value.trim();
|
| 468 |
if (!text) { showToast('أدخل آية للتدقيق'); return; }
|
| 469 |
|
|
|
|
| 470 |
setLoading(true, 'جارٍ التدقيق...');
|
|
|
|
|
|
|
|
|
|
| 471 |
try {
|
| 472 |
const data = await bayanQuran(text);
|
| 473 |
quranResultSection.classList.remove('is-hidden');
|
|
|
|
| 474 |
if (data.error) {
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
quranText.textContent = data.full_verse || data.matched_segment || JSON.stringify(data);
|
| 479 |
-
quranMeta.textContent = data.matched_segment && data.full_verse
|
| 480 |
-
? `المقطع المطابق: ${data.matched_segment}`
|
| 481 |
-
: '';
|
| 482 |
-
showToast('✓ تم التدقيق');
|
| 483 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
} catch (error) {
|
| 485 |
console.error('[Bayan] Quran error:', error);
|
| 486 |
-
|
|
|
|
|
|
|
| 487 |
} finally {
|
| 488 |
setLoading(false);
|
| 489 |
}
|
| 490 |
});
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
btnCopyQuran.addEventListener('click', () => {
|
| 493 |
-
|
|
|
|
| 494 |
.then(() => showToast('✓ تم النسخ'))
|
| 495 |
.catch(() => showToast('تعذّر النسخ'));
|
| 496 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
}
|
| 498 |
|
| 499 |
// ══════════════════════════════════════════════════════════
|
| 500 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
// ══════════════════════════════════════════════════════════
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
const
|
| 505 |
-
const
|
| 506 |
-
const acList = document.getElementById('autocomplete-list');
|
| 507 |
|
| 508 |
-
if (
|
| 509 |
-
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
const suggestions = data.suggestions || [];
|
| 519 |
-
acResultSection.classList.remove('is-hidden');
|
| 520 |
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
|
|
|
| 525 |
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
acList.querySelectorAll('.bayan-ac-chip').forEach((chip) => {
|
| 531 |
-
chip.addEventListener('click', () => {
|
| 532 |
-
const needsSpace = acInput.value.length > 0 && !/\s$/.test(acInput.value);
|
| 533 |
-
acInput.value += (needsSpace ? ' ' : '') + chip.textContent;
|
| 534 |
-
updateCounts(acInput, acCharCount, null);
|
| 535 |
-
acInput.focus();
|
| 536 |
-
showToast('✓ تمت الإضافة');
|
| 537 |
-
});
|
| 538 |
-
});
|
| 539 |
-
} catch (error) {
|
| 540 |
-
console.error('[Bayan] Autocomplete error:', error);
|
| 541 |
-
showToast('خطأ في الاتصال — تحقق من الإنترنت');
|
| 542 |
-
} finally {
|
| 543 |
-
setLoading(false);
|
| 544 |
-
}
|
| 545 |
});
|
| 546 |
}
|
| 547 |
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
}
|
| 569 |
}
|
| 570 |
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
if (!headerEl) return;
|
| 575 |
-
const btn = document.createElement('button');
|
| 576 |
-
btn.className = 'bayan-btn-icon';
|
| 577 |
-
btn.type = 'button';
|
| 578 |
-
btn.title = 'تنزيل كملف نصي';
|
| 579 |
-
btn.innerHTML = DOWNLOAD_ICON;
|
| 580 |
-
btn.addEventListener('click', () => downloadTxt((getText() || '').trim(), filename));
|
| 581 |
-
headerEl.appendChild(btn);
|
| 582 |
}
|
| 583 |
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
()
|
| 592 |
-
|
| 593 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
|
| 595 |
// ══════════════════════════════════════════════════════════
|
| 596 |
// Status check on load
|
|
@@ -661,7 +864,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 661 |
} else if (data.contextAction === TAB_ACTIONS.summarize) {
|
| 662 |
// Fill the summary tab and auto-trigger summarization
|
| 663 |
summaryInputText.value = data.contextText;
|
| 664 |
-
|
|
|
|
| 665 |
|
| 666 |
// Switch to summarize tab
|
| 667 |
const summarizeTab = document.querySelector(`[data-tab="${TAB_ACTIONS.summarize}"]`);
|
|
@@ -708,4 +912,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 708 |
chrome.storage.local.set({ theme: targetTheme });
|
| 709 |
});
|
| 710 |
}
|
| 711 |
-
})();
|
|
|
|
| 15 |
const btnCorrect = document.getElementById('btn-correct');
|
| 16 |
const btnClear = document.getElementById('btn-clear');
|
| 17 |
const btnApplyAll = document.getElementById('btn-apply-all');
|
| 18 |
+
const btnCopyText = document.getElementById('btn-copy-text');
|
| 19 |
const scoreSection = document.getElementById('score-section');
|
|
|
|
|
|
|
| 20 |
const suggestionsSection = document.getElementById('suggestions-section');
|
| 21 |
const suggestionsList = document.getElementById('suggestions-list');
|
| 22 |
const timingSection = document.getElementById('timing-section');
|
|
|
|
| 26 |
|
| 27 |
// Summary tab elements
|
| 28 |
const summaryInputText = document.getElementById('summary-input-text');
|
| 29 |
+
const summaryWordCountInput = document.getElementById('summary-word-count-input');
|
| 30 |
const btnSummarize = document.getElementById('btn-summarize');
|
| 31 |
const summaryResultSection = document.getElementById('summary-result-section');
|
| 32 |
const summaryText = document.getElementById('summary-text');
|
| 33 |
+
const summaryStats = document.getElementById('summary-stats');
|
| 34 |
+
const summaryWordCount = document.getElementById('summary-word-count');
|
| 35 |
+
const summaryCompression = document.getElementById('summary-compression');
|
| 36 |
const btnCopySummary = document.getElementById('btn-copy-summary');
|
| 37 |
|
| 38 |
// Score elements
|
|
|
|
| 64 |
|
| 65 |
const SCORE_CIRCUMFERENCE = 440;
|
| 66 |
|
| 67 |
+
// ══════════════════════════════════════════════════════════
|
| 68 |
+
// State persistence — survive popup close/reopen
|
| 69 |
+
// ══════════════════════════════════════════════════════════
|
| 70 |
+
const STORAGE_KEY = 'bayan_popup_state';
|
| 71 |
+
|
| 72 |
+
function saveState() {
|
| 73 |
+
const activeTab = document.querySelector('.bayan-tab.active');
|
| 74 |
+
const state = {
|
| 75 |
+
tab: activeTab ? activeTab.dataset.tab : 'correct',
|
| 76 |
+
inputText: inputText.value,
|
| 77 |
+
summaryInput: summaryInputText.value,
|
| 78 |
+
dialectInput: document.getElementById('dialect-input-text')?.value || '',
|
| 79 |
+
quranInput: document.getElementById('quran-input-text')?.value || '',
|
| 80 |
+
};
|
| 81 |
+
try {
|
| 82 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
| 83 |
+
} catch { /* quota exceeded — ignore */ }
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function restoreState() {
|
| 87 |
+
try {
|
| 88 |
+
const raw = localStorage.getItem(STORAGE_KEY);
|
| 89 |
+
if (!raw) return;
|
| 90 |
+
const state = JSON.parse(raw);
|
| 91 |
+
|
| 92 |
+
if (state.inputText) {
|
| 93 |
+
inputText.value = state.inputText;
|
| 94 |
+
updateCounts(inputText, charCount, wordCount);
|
| 95 |
+
}
|
| 96 |
+
if (state.summaryInput) {
|
| 97 |
+
summaryInputText.value = state.summaryInput;
|
| 98 |
+
const words = state.summaryInput.trim() ? state.summaryInput.trim().split(/\s+/).length : 0;
|
| 99 |
+
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
|
| 100 |
+
}
|
| 101 |
+
const dialectEl = document.getElementById('dialect-input-text');
|
| 102 |
+
if (state.dialectInput && dialectEl) dialectEl.value = state.dialectInput;
|
| 103 |
+
const quranEl = document.getElementById('quran-input-text');
|
| 104 |
+
if (state.quranInput && quranEl) quranEl.value = state.quranInput;
|
| 105 |
+
|
| 106 |
+
if (state.tab && state.tab !== 'correct') {
|
| 107 |
+
const tabBtn = document.querySelector(`.bayan-tab[data-tab="${state.tab}"]`);
|
| 108 |
+
if (tabBtn) tabBtn.click();
|
| 109 |
+
}
|
| 110 |
+
} catch { /* corrupt state — ignore */ }
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
restoreState();
|
| 114 |
+
|
| 115 |
// ══════════════════════════════════════════════════════════
|
| 116 |
// Tab switching
|
| 117 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 125 |
document.querySelectorAll('.bayan-panel').forEach((p) => {
|
| 126 |
p.classList.toggle('active', p.id === `panel-${targetTab}`);
|
| 127 |
});
|
| 128 |
+
saveState();
|
| 129 |
});
|
| 130 |
});
|
| 131 |
|
|
|
|
| 142 |
|
| 143 |
inputText.addEventListener('input', () => {
|
| 144 |
updateCounts(inputText, charCount, wordCount);
|
| 145 |
+
saveState();
|
| 146 |
|
| 147 |
// MED-2: Detect user edit after analysis → mark stale
|
| 148 |
if (currentSuggestions.length > 0 && inputText.value !== analyzedText) {
|
|
|
|
| 150 |
}
|
| 151 |
});
|
| 152 |
|
| 153 |
+
summaryInputText.addEventListener('input', () => {
|
| 154 |
+
const text = summaryInputText.value.trim();
|
| 155 |
+
const words = text ? text.split(/\s+/).length : 0;
|
| 156 |
+
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
|
| 157 |
+
saveState();
|
| 158 |
+
});
|
| 159 |
|
| 160 |
// ══════════════════════════════════════════════════════════
|
| 161 |
// MED-2: Staleness management
|
|
|
|
| 166 |
* Disables suggestion actions and shows a re-analysis prompt.
|
| 167 |
*/
|
| 168 |
function markStale() {
|
| 169 |
+
if (isStale) return;
|
| 170 |
isStale = true;
|
| 171 |
|
|
|
|
|
|
|
| 172 |
if (suggestionsSection) suggestionsSection.classList.add('bayan-stale');
|
| 173 |
|
|
|
|
| 174 |
showToast('⚠ النص تغيّر — أعد التحليل لتحديث الاقتراحات', 4000);
|
| 175 |
|
|
|
|
| 176 |
btnCorrect.innerHTML = `
|
| 177 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h5M20 20v-5h-5M4 9a8 8 0 0114-3M20 15a8 8 0 01-14 3"/></svg>
|
| 178 |
إعادة التحليل`;
|
|
|
|
| 183 |
*/
|
| 184 |
function clearStale() {
|
| 185 |
isStale = false;
|
|
|
|
| 186 |
if (suggestionsSection) suggestionsSection.classList.remove('bayan-stale');
|
| 187 |
|
|
|
|
| 188 |
btnCorrect.innerHTML = `
|
| 189 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4"/></svg>
|
| 190 |
تحليل وتصحيح`;
|
|
|
|
| 285 |
updateScore(counts.spelling, counts.grammar, counts.punctuation);
|
| 286 |
renderSuggestions(currentSuggestions);
|
| 287 |
|
|
|
|
|
|
|
|
|
|
| 288 |
showToast('✓ تم التصحيح');
|
| 289 |
+
saveState();
|
| 290 |
});
|
| 291 |
});
|
| 292 |
|
|
|
|
| 302 |
analyzedText = '';
|
| 303 |
updateCounts(inputText, charCount, wordCount);
|
| 304 |
scoreSection.classList.add('is-hidden');
|
|
|
|
| 305 |
suggestionsSection.classList.add('is-hidden');
|
| 306 |
timingSection.classList.add('is-hidden');
|
| 307 |
currentSuggestions = [];
|
| 308 |
clearStale();
|
| 309 |
+
saveState();
|
| 310 |
});
|
| 311 |
|
| 312 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 318 |
showToast('أدخل نصاً للتحليل');
|
| 319 |
return;
|
| 320 |
}
|
| 321 |
+
if (text.trim().split(/\s+/).length < 2) {
|
| 322 |
+
showToast('أدخل كلمتين على الأقل');
|
| 323 |
return;
|
| 324 |
}
|
| 325 |
if (text.length > CONFIG.MAX_ANALYZE_LENGTH) {
|
|
|
|
| 343 |
inputText.value = analyzedText;
|
| 344 |
updateCounts(inputText, charCount, wordCount);
|
| 345 |
|
| 346 |
+
// Show corrected text in textarea
|
| 347 |
+
// (suggestions apply directly to textarea via chips or Apply All)
|
|
|
|
| 348 |
|
| 349 |
// Update score
|
| 350 |
const counts = countByType(suggestions);
|
|
|
|
| 362 |
if (suggestions.length === 0) {
|
| 363 |
showToast('نصك ممتاز! لم نجد أي أخطاء ✨');
|
| 364 |
}
|
| 365 |
+
|
| 366 |
+
saveState();
|
| 367 |
} else {
|
| 368 |
showToast('تعذّر التحليل — حاول مرة أخرى');
|
| 369 |
}
|
|
|
|
| 392 |
inputText.value = analyzedText;
|
| 393 |
updateCounts(inputText, charCount, wordCount);
|
| 394 |
currentSuggestions = [];
|
|
|
|
| 395 |
updateScore(0, 0, 0);
|
| 396 |
renderSuggestions([]);
|
| 397 |
showToast('✓ تم تطبيق جميع التصحيحات');
|
| 398 |
+
saveState();
|
| 399 |
});
|
| 400 |
|
| 401 |
// ══════════════════════════════════════════════════════════
|
| 402 |
+
// Copy text from textarea (MED-1: .catch() for clipboard errors)
|
| 403 |
// ══════════════════════════════════════════════════════════
|
| 404 |
+
btnCopyText.addEventListener('click', () => {
|
| 405 |
+
const text = inputText.value || '';
|
| 406 |
navigator.clipboard.writeText(text)
|
| 407 |
.then(() => showToast('✓ تم نسخ النص'))
|
| 408 |
.catch(() => showToast('تعذّر النسخ'));
|
| 409 |
});
|
| 410 |
|
| 411 |
+
// ══════════════════════════════════════════════════════════
|
| 412 |
+
// Summary mode toggle (paragraph / bullets)
|
| 413 |
+
// ══════════════════════════════════════════════════════════
|
| 414 |
+
let summaryMode = 'paragraph';
|
| 415 |
+
|
| 416 |
+
document.querySelectorAll('.bayan-mode-btn').forEach((btn) => {
|
| 417 |
+
btn.addEventListener('click', () => {
|
| 418 |
+
summaryMode = btn.dataset.mode;
|
| 419 |
+
document.querySelectorAll('.bayan-mode-btn').forEach((b) => {
|
| 420 |
+
b.classList.toggle('active', b.dataset.mode === summaryMode);
|
| 421 |
+
});
|
| 422 |
+
});
|
| 423 |
+
});
|
| 424 |
+
|
| 425 |
// ══════════════════════════════════════════════════════════
|
| 426 |
// Summarize button
|
| 427 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 445 |
|
| 446 |
if (data.status === 'success' && data.summary) {
|
| 447 |
summaryResultSection.classList.remove('is-hidden');
|
| 448 |
+
|
| 449 |
+
const summaryContent = data.summary;
|
| 450 |
+
|
| 451 |
+
if (summaryMode === 'bullets') {
|
| 452 |
+
const sentences = summaryContent.split(/[.،؛]\s*/).filter(s => s.trim().length > 2);
|
| 453 |
+
const ul = document.createElement('ul');
|
| 454 |
+
ul.style.cssText = 'list-style: disc; padding-right: 1.5rem; direction: rtl; text-align: right;';
|
| 455 |
+
sentences.forEach(s => {
|
| 456 |
+
const li = document.createElement('li');
|
| 457 |
+
li.textContent = s.trim();
|
| 458 |
+
li.style.marginBottom = '8px';
|
| 459 |
+
ul.appendChild(li);
|
| 460 |
+
});
|
| 461 |
+
summaryText.textContent = '';
|
| 462 |
+
summaryText.appendChild(ul);
|
| 463 |
+
} else {
|
| 464 |
+
summaryText.textContent = summaryContent;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
const origWords = text.trim().split(/\s+/).length;
|
| 468 |
+
const sumWords = summaryContent.trim().split(/\s+/).length;
|
| 469 |
+
const compression = origWords > 0 ? Math.round((1 - sumWords / origWords) * 100) : 0;
|
| 470 |
+
|
| 471 |
+
if (summaryStats) {
|
| 472 |
+
summaryStats.classList.remove('is-hidden');
|
| 473 |
+
if (summaryWordCount) summaryWordCount.textContent = sumWords.toLocaleString('ar-EG');
|
| 474 |
+
if (summaryCompression) summaryCompression.textContent = compression.toLocaleString('ar-EG') + '٪';
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
showToast('✓ تم التلخيص');
|
| 478 |
} else {
|
| 479 |
showToast('تعذّر التلخيص — حاول مرة أخرى');
|
|
|
|
| 490 |
// Copy summary (MED-1: .catch() for clipboard errors)
|
| 491 |
// ══════════════════════════════════════════════════════════
|
| 492 |
btnCopySummary.addEventListener('click', () => {
|
| 493 |
+
const text = summaryText.innerText || summaryText.textContent || '';
|
| 494 |
navigator.clipboard.writeText(text)
|
| 495 |
.then(() => showToast('✓ تم نسخ الملخص'))
|
| 496 |
.catch(() => showToast('تعذّر النسخ'));
|
|
|
|
| 507 |
const btnCopyDialect = document.getElementById('btn-copy-dialect');
|
| 508 |
|
| 509 |
if (dialectInput) {
|
| 510 |
+
dialectInput.addEventListener('input', () => {
|
| 511 |
+
const chars = dialectInput.value.length;
|
| 512 |
+
if (dialectCharCount) dialectCharCount.textContent = chars.toLocaleString('ar-EG');
|
| 513 |
+
saveState();
|
| 514 |
+
});
|
| 515 |
|
| 516 |
btnDialect.addEventListener('click', async () => {
|
| 517 |
const text = dialectInput.value.trim();
|
|
|
|
| 543 |
}
|
| 544 |
|
| 545 |
// ═══════════════════════════════════════════════════════���══
|
| 546 |
+
// Quran verification + translation (matching website)
|
| 547 |
// ══════════════════════════════════════════════════════════
|
| 548 |
const quranInput = document.getElementById('quran-input-text');
|
| 549 |
const quranCharCount = document.getElementById('quran-char-count');
|
| 550 |
const btnQuran = document.getElementById('btn-quran');
|
| 551 |
const quranResultSection = document.getElementById('quran-result-section');
|
| 552 |
+
const quranUthmani = document.getElementById('quran-uthmani');
|
| 553 |
+
const quranReference = document.getElementById('quran-reference');
|
| 554 |
const btnCopyQuran = document.getElementById('btn-copy-quran');
|
| 555 |
+
const quranLangSelect = document.getElementById('quran-lang-select');
|
| 556 |
+
const quranTransSection = document.getElementById('quran-translation-section');
|
| 557 |
+
const quranTransText = document.getElementById('quran-trans-text');
|
| 558 |
+
const quranTransRef = document.getElementById('quran-trans-ref');
|
| 559 |
+
|
| 560 |
+
let _quranQuery = '';
|
| 561 |
+
let _quranVerse = '';
|
| 562 |
+
let _quranRef = '';
|
| 563 |
+
let _quranTransText = '';
|
| 564 |
+
let _quranTransRef = '';
|
| 565 |
|
| 566 |
if (quranInput) {
|
| 567 |
+
quranInput.addEventListener('input', () => { updateCounts(quranInput, quranCharCount, null); saveState(); });
|
| 568 |
|
| 569 |
btnQuran.addEventListener('click', async () => {
|
| 570 |
const text = quranInput.value.trim();
|
| 571 |
if (!text) { showToast('أدخل آية للتدقيق'); return; }
|
| 572 |
|
| 573 |
+
_quranQuery = text;
|
| 574 |
setLoading(true, 'جارٍ التدقيق...');
|
| 575 |
+
quranTransSection.classList.add('is-hidden');
|
| 576 |
+
quranLangSelect.value = '';
|
| 577 |
+
|
| 578 |
try {
|
| 579 |
const data = await bayanQuran(text);
|
| 580 |
quranResultSection.classList.remove('is-hidden');
|
| 581 |
+
|
| 582 |
if (data.error) {
|
| 583 |
+
quranUthmani.textContent = data.error;
|
| 584 |
+
quranReference.textContent = '';
|
| 585 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
}
|
| 587 |
+
|
| 588 |
+
const seg = data.matched_segment || '';
|
| 589 |
+
const refMatch = seg.match(/【([^】]+)】/);
|
| 590 |
+
const verseText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 591 |
+
const reference = refMatch ? refMatch[1] : '';
|
| 592 |
+
|
| 593 |
+
_quranVerse = verseText;
|
| 594 |
+
_quranRef = reference;
|
| 595 |
+
quranUthmani.textContent = verseText;
|
| 596 |
+
quranReference.textContent = reference ? `[${reference}]` : '';
|
| 597 |
+
showToast('✓ تم التدقيق');
|
| 598 |
} catch (error) {
|
| 599 |
console.error('[Bayan] Quran error:', error);
|
| 600 |
+
quranResultSection.classList.remove('is-hidden');
|
| 601 |
+
quranUthmani.textContent = 'خطأ في الاتصال — تحقق من الإنترنت';
|
| 602 |
+
quranReference.textContent = '';
|
| 603 |
} finally {
|
| 604 |
setLoading(false);
|
| 605 |
}
|
| 606 |
});
|
| 607 |
|
| 608 |
+
// Translation dropdown
|
| 609 |
+
quranLangSelect.addEventListener('change', async () => {
|
| 610 |
+
const lang = quranLangSelect.value;
|
| 611 |
+
if (!lang || !_quranQuery) return;
|
| 612 |
+
|
| 613 |
+
quranTransSection.classList.remove('is-hidden');
|
| 614 |
+
quranTransText.textContent = '⏳ جاري الترجمة...';
|
| 615 |
+
if (quranTransRef) quranTransRef.style.display = 'none';
|
| 616 |
+
|
| 617 |
+
try {
|
| 618 |
+
const data = await bayanQuran(_quranQuery, lang);
|
| 619 |
+
|
| 620 |
+
if (data.error) {
|
| 621 |
+
quranTransText.textContent = data.error;
|
| 622 |
+
return;
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
const seg = data.matched_segment || '';
|
| 626 |
+
const refMatch = seg.match(/【([^】]+)】/);
|
| 627 |
+
const transText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 628 |
+
const transRef = refMatch ? refMatch[1] : '';
|
| 629 |
+
|
| 630 |
+
_quranTransText = transText;
|
| 631 |
+
_quranTransRef = transRef;
|
| 632 |
+
|
| 633 |
+
quranTransText.textContent = transText;
|
| 634 |
+
if (quranTransRef && transRef) {
|
| 635 |
+
quranTransRef.textContent = `[${transRef}]`;
|
| 636 |
+
quranTransRef.style.display = '';
|
| 637 |
+
}
|
| 638 |
+
const transActions = document.getElementById('quran-trans-actions');
|
| 639 |
+
if (transActions) transActions.style.display = 'flex';
|
| 640 |
+
} catch (error) {
|
| 641 |
+
console.error('[Bayan] Quran translation error:', error);
|
| 642 |
+
quranTransText.textContent = 'حدث خطأ في الترجمة';
|
| 643 |
+
}
|
| 644 |
+
});
|
| 645 |
+
|
| 646 |
btnCopyQuran.addEventListener('click', () => {
|
| 647 |
+
const text = (_quranVerse || '') + (_quranRef ? ` [${_quranRef}]` : '');
|
| 648 |
+
navigator.clipboard.writeText(text)
|
| 649 |
.then(() => showToast('✓ تم النسخ'))
|
| 650 |
.catch(() => showToast('تعذّر النسخ'));
|
| 651 |
});
|
| 652 |
+
|
| 653 |
+
const btnCopyQuranTrans = document.getElementById('btn-copy-quran-trans');
|
| 654 |
+
if (btnCopyQuranTrans) {
|
| 655 |
+
btnCopyQuranTrans.addEventListener('click', () => {
|
| 656 |
+
const text = (_quranTransText || '') + (_quranTransRef ? ` [${_quranTransRef}]` : '');
|
| 657 |
+
navigator.clipboard.writeText(text)
|
| 658 |
+
.then(() => showToast('✓ تم نسخ الترجمة'))
|
| 659 |
+
.catch(() => showToast('تعذّر النسخ'));
|
| 660 |
+
});
|
| 661 |
+
}
|
| 662 |
}
|
| 663 |
|
| 664 |
// ══════════════════════════════════════════════════════════
|
| 665 |
+
// Summary: File import (.txt / .docx)
|
| 666 |
+
// ══════════════════════════════════════════════════════════
|
| 667 |
+
const summaryImportInput = document.getElementById('summary-import-input');
|
| 668 |
+
if (summaryImportInput) {
|
| 669 |
+
summaryImportInput.addEventListener('change', (e) => {
|
| 670 |
+
const file = e.target.files && e.target.files[0];
|
| 671 |
+
if (!file) return;
|
| 672 |
+
|
| 673 |
+
if (file.name.endsWith('.txt')) {
|
| 674 |
+
const reader = new FileReader();
|
| 675 |
+
reader.onload = (ev) => {
|
| 676 |
+
summaryInputText.value = ev.target.result;
|
| 677 |
+
const words = summaryInputText.value.trim() ? summaryInputText.value.trim().split(/\s+/).length : 0;
|
| 678 |
+
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
|
| 679 |
+
showToast('✓ تم استيراد الملف');
|
| 680 |
+
saveState();
|
| 681 |
+
};
|
| 682 |
+
reader.readAsText(file, 'UTF-8');
|
| 683 |
+
} else if (file.name.endsWith('.docx')) {
|
| 684 |
+
if (typeof mammoth !== 'undefined') {
|
| 685 |
+
const reader = new FileReader();
|
| 686 |
+
reader.onload = (ev) => {
|
| 687 |
+
mammoth.extractRawText({ arrayBuffer: ev.target.result })
|
| 688 |
+
.then((result) => {
|
| 689 |
+
summaryInputText.value = result.value;
|
| 690 |
+
const words = result.value.trim() ? result.value.trim().split(/\s+/).length : 0;
|
| 691 |
+
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
|
| 692 |
+
showToast('✓ تم استيراد الملف');
|
| 693 |
+
saveState();
|
| 694 |
+
})
|
| 695 |
+
.catch(() => showToast('خطأ في قراءة الملف'));
|
| 696 |
+
};
|
| 697 |
+
reader.readAsArrayBuffer(file);
|
| 698 |
+
} else {
|
| 699 |
+
showToast('صيغة .docx غير مدعومة — استخدم .txt');
|
| 700 |
+
}
|
| 701 |
+
}
|
| 702 |
+
e.target.value = '';
|
| 703 |
+
});
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
// ══════════════════════════════════════════════════════════
|
| 707 |
+
// Summary: Export dropdown (.txt / .docx / .pdf)
|
| 708 |
+
// ══════════════════════════════════════════════════════════
|
| 709 |
+
const btnExportSummary = document.getElementById('btn-export-summary');
|
| 710 |
+
const summaryExportMenu = document.getElementById('summary-export-menu');
|
|
|
|
| 711 |
|
| 712 |
+
if (btnExportSummary && summaryExportMenu) {
|
| 713 |
+
btnExportSummary.addEventListener('click', (e) => {
|
| 714 |
+
e.stopPropagation();
|
| 715 |
+
summaryExportMenu.classList.toggle('is-hidden');
|
| 716 |
+
});
|
| 717 |
|
| 718 |
+
document.addEventListener('click', () => {
|
| 719 |
+
summaryExportMenu.classList.add('is-hidden');
|
| 720 |
+
});
|
| 721 |
|
| 722 |
+
summaryExportMenu.addEventListener('click', (e) => {
|
| 723 |
+
e.stopPropagation();
|
| 724 |
+
});
|
|
|
|
|
|
|
| 725 |
|
| 726 |
+
summaryExportMenu.querySelectorAll('.bayan-export-item').forEach((item) => {
|
| 727 |
+
item.addEventListener('click', () => {
|
| 728 |
+
const format = item.dataset.format;
|
| 729 |
+
const text = (summaryText.innerText || summaryText.textContent || '').trim();
|
| 730 |
+
if (!text) { showToast('لا يوجد ملخص للتصدير'); return; }
|
| 731 |
|
| 732 |
+
summaryExportMenu.classList.add('is-hidden');
|
| 733 |
+
exportSummary(format, text);
|
| 734 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
});
|
| 736 |
}
|
| 737 |
|
| 738 |
+
function exportSummary(format, text) {
|
| 739 |
+
if (format === 'txt') {
|
| 740 |
+
downloadFile(text, 'ملخص-بيان.txt', 'text/plain;charset=utf-8');
|
| 741 |
+
showToast('✓ تم تصدير الملخص');
|
| 742 |
+
} else if (format === 'docx') {
|
| 743 |
+
if (typeof docx === 'undefined') { showToast('مكتبة Word غير محمّلة'); return; }
|
| 744 |
+
try {
|
| 745 |
+
const paragraphs = text.split(/\n+/).filter(p => p.trim());
|
| 746 |
+
const children = paragraphs.map(block =>
|
| 747 |
+
new docx.Paragraph({
|
| 748 |
+
bidirectional: true,
|
| 749 |
+
alignment: docx.AlignmentType.RIGHT,
|
| 750 |
+
children: [new docx.TextRun({ text: block, rightToLeft: true, font: 'Arial' })]
|
| 751 |
+
})
|
| 752 |
+
);
|
| 753 |
+
const doc = new docx.Document({ sections: [{ properties: { rightToLeft: true }, children }] });
|
| 754 |
+
docx.Packer.toBlob(doc).then((blob) => {
|
| 755 |
+
downloadBlob(blob, 'ملخص-بيان.docx');
|
| 756 |
+
showToast('✓ تم تصدير الملخص كـ Word');
|
| 757 |
+
}).catch(() => showToast('تعذر تصدير ملف Word'));
|
| 758 |
+
} catch { showToast('تعذر تصدير ملف Word'); }
|
| 759 |
+
} else if (format === 'pdf') {
|
| 760 |
+
if (typeof html2pdf === 'undefined') { showToast('مكتبة PDF غير محمّلة'); return; }
|
| 761 |
+
showToast('جاري تصدير PDF...');
|
| 762 |
+
const html = '<div dir="rtl" style="font-family:Arial,sans-serif;font-size:16px;line-height:2;text-align:right;padding:20px;">' +
|
| 763 |
+
text.split(/\n+/).map(p => '<p>' + p + '</p>').join('') + '</div>';
|
| 764 |
+
html2pdf().set({
|
| 765 |
+
margin: [15, 15, 15, 15],
|
| 766 |
+
filename: 'ملخص-بيان.pdf',
|
| 767 |
+
image: { type: 'jpeg', quality: 0.95 },
|
| 768 |
+
html2canvas: { scale: 1.5, useCORS: true, backgroundColor: '#ffffff' },
|
| 769 |
+
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
| 770 |
+
}).from(html, 'string').save()
|
| 771 |
+
.then(() => showToast('✓ تم تصدير الملخص كـ PDF'))
|
| 772 |
+
.catch(() => showToast('تعذر تصدير PDF'));
|
| 773 |
}
|
| 774 |
}
|
| 775 |
|
| 776 |
+
function downloadFile(text, filename, mime) {
|
| 777 |
+
const blob = new Blob([text], { type: mime });
|
| 778 |
+
downloadBlob(blob, filename);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 779 |
}
|
| 780 |
|
| 781 |
+
function downloadBlob(blob, filename) {
|
| 782 |
+
const url = URL.createObjectURL(blob);
|
| 783 |
+
const a = document.createElement('a');
|
| 784 |
+
a.href = url;
|
| 785 |
+
a.download = filename;
|
| 786 |
+
document.body.appendChild(a);
|
| 787 |
+
a.click();
|
| 788 |
+
a.remove();
|
| 789 |
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
// ── Close button: close the popup window ──
|
| 793 |
+
const btnClosePopup = document.getElementById('btn-close-popup');
|
| 794 |
+
if (btnClosePopup) {
|
| 795 |
+
btnClosePopup.addEventListener('click', () => window.close());
|
| 796 |
+
}
|
| 797 |
|
| 798 |
// ══════════════════════════════════════════════════════════
|
| 799 |
// Status check on load
|
|
|
|
| 864 |
} else if (data.contextAction === TAB_ACTIONS.summarize) {
|
| 865 |
// Fill the summary tab and auto-trigger summarization
|
| 866 |
summaryInputText.value = data.contextText;
|
| 867 |
+
const ctxWords = data.contextText.trim() ? data.contextText.trim().split(/\s+/).length : 0;
|
| 868 |
+
if (summaryWordCountInput) summaryWordCountInput.textContent = ctxWords.toLocaleString('ar-EG');
|
| 869 |
|
| 870 |
// Switch to summarize tab
|
| 871 |
const summarizeTab = document.querySelector(`[data-tab="${TAB_ACTIONS.summarize}"]`);
|
|
|
|
| 912 |
chrome.storage.local.set({ theme: targetTheme });
|
| 913 |
});
|
| 914 |
}
|
| 915 |
+
})();
|
extension/shared/bayan-state.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Bayan Chrome Extension — Unified State Manager
|
| 3 |
-
*
|
| 4 |
-
* Phase 7: Production Hardening
|
| 5 |
-
*
|
| 6 |
-
* Single source of truth for runtime state across the inline engine.
|
| 7 |
-
* Loaded as a content script BEFORE content-inline.js.
|
| 8 |
-
*
|
| 9 |
-
* Responsibilities:
|
| 10 |
-
* - Track active field reference (WeakRef for GC safety)
|
| 11 |
-
* - Store last analyzed text + response
|
| 12 |
-
* - Track active mode
|
| 13 |
-
* - Provide clean teardown
|
| 14 |
-
*/
|
| 15 |
-
|
| 16 |
-
// eslint-disable-next-line no-unused-vars
|
| 17 |
-
const BayanState = (() => {
|
| 18 |
-
'use strict';
|
| 19 |
-
|
| 20 |
-
/** @type {WeakRef<HTMLElement>|null} */
|
| 21 |
-
let _fieldRef = null;
|
| 22 |
-
let _lastText = '';
|
| 23 |
-
let _lastHash = '';
|
| 24 |
-
let _lastResponse = null;
|
| 25 |
-
/** @type {Array} */
|
| 26 |
-
let _suggestions = [];
|
| 27 |
-
let _mode = 'idle'; // idle | inline | sidepanel | contextmenu
|
| 28 |
-
let _paused = false;
|
| 29 |
-
let _pauseReason = '';
|
| 30 |
-
|
| 31 |
-
/**
|
| 32 |
-
* Simple FNV-1a hash for text deduplication.
|
| 33 |
-
* 32-bit, fast, zero dependencies.
|
| 34 |
-
* @param {string} str
|
| 35 |
-
* @returns {string}
|
| 36 |
-
*/
|
| 37 |
-
function hash(str) {
|
| 38 |
-
let h = 0x811c9dc5;
|
| 39 |
-
for (let i = 0; i < str.length; i++) {
|
| 40 |
-
h ^= str.charCodeAt(i);
|
| 41 |
-
h = Math.imul(h, 0x01000193);
|
| 42 |
-
}
|
| 43 |
-
return (h >>> 0).toString(36);
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
return {
|
| 47 |
-
// ── Field management ──
|
| 48 |
-
/** @param {HTMLElement} field */
|
| 49 |
-
setField(field) {
|
| 50 |
-
_fieldRef = field ? new WeakRef(field) : null;
|
| 51 |
-
},
|
| 52 |
-
|
| 53 |
-
/** @returns {HTMLElement|null} */
|
| 54 |
-
getField() {
|
| 55 |
-
return _fieldRef?.deref() ?? null;
|
| 56 |
-
},
|
| 57 |
-
|
| 58 |
-
hasField() {
|
| 59 |
-
return !!(_fieldRef?.deref());
|
| 60 |
-
},
|
| 61 |
-
|
| 62 |
-
// ── Text + hash ──
|
| 63 |
-
setLastText(text) {
|
| 64 |
-
_lastText = text;
|
| 65 |
-
_lastHash = hash(text);
|
| 66 |
-
},
|
| 67 |
-
getLastText() { return _lastText; },
|
| 68 |
-
getLastHash() { return _lastHash; },
|
| 69 |
-
|
| 70 |
-
/**
|
| 71 |
-
* Check if given text is identical to last analyzed text.
|
| 72 |
-
* Uses hash comparison (O(1)) instead of string equality (O(n)).
|
| 73 |
-
*/
|
| 74 |
-
isDuplicate(text) {
|
| 75 |
-
return hash(text) === _lastHash && text === _lastText;
|
| 76 |
-
},
|
| 77 |
-
|
| 78 |
-
// ── Response cache ──
|
| 79 |
-
setLastResponse(data) { _lastResponse = data; },
|
| 80 |
-
getLastResponse() { return _lastResponse; },
|
| 81 |
-
|
| 82 |
-
// ── Suggestions ──
|
| 83 |
-
setSuggestions(suggestions) { _suggestions = suggestions || []; },
|
| 84 |
-
getSuggestions() { return _suggestions; },
|
| 85 |
-
|
| 86 |
-
// ── Mode ──
|
| 87 |
-
setMode(mode) { _mode = mode; },
|
| 88 |
-
getMode() { return _mode; },
|
| 89 |
-
|
| 90 |
-
// ── Pause state ──
|
| 91 |
-
pause(reason) {
|
| 92 |
-
_paused = true;
|
| 93 |
-
_pauseReason = reason || 'unknown';
|
| 94 |
-
},
|
| 95 |
-
resume() {
|
| 96 |
-
_paused = false;
|
| 97 |
-
_pauseReason = '';
|
| 98 |
-
},
|
| 99 |
-
isPaused() { return _paused; },
|
| 100 |
-
getPauseReason() { return _pauseReason; },
|
| 101 |
-
|
| 102 |
-
// ── Clean teardown ──
|
| 103 |
-
reset() {
|
| 104 |
-
_fieldRef = null;
|
| 105 |
-
_lastText = '';
|
| 106 |
-
_lastHash = '';
|
| 107 |
-
_lastResponse = null;
|
| 108 |
-
_suggestions = [];
|
| 109 |
-
_mode = 'idle';
|
| 110 |
-
_paused = false;
|
| 111 |
-
_pauseReason = '';
|
| 112 |
-
},
|
| 113 |
-
|
| 114 |
-
// ── Debug ──
|
| 115 |
-
toJSON() {
|
| 116 |
-
return {
|
| 117 |
-
hasField: !!(_fieldRef?.deref()),
|
| 118 |
-
textLength: _lastText.length,
|
| 119 |
-
hash: _lastHash,
|
| 120 |
-
suggestions: _suggestions.length,
|
| 121 |
-
mode: _mode,
|
| 122 |
-
paused: _paused,
|
| 123 |
-
pauseReason: _pauseReason,
|
| 124 |
-
};
|
| 125 |
-
},
|
| 126 |
-
};
|
| 127 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/shared/css/tokens.css
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Bayan Design System — Theme Tokens
|
| 3 |
+
* Comfortable palettes for long Arabic reading (Notion / Grammarly inspired)
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
/* ── Shared structural tokens ── */
|
| 7 |
+
:root {
|
| 8 |
+
--font-family-primary: 'Cairo', 'Tajawal', 'Noto Sans Arabic', sans-serif;
|
| 9 |
+
|
| 10 |
+
--font-size-display: 3rem;
|
| 11 |
+
--font-size-h1: 2.25rem;
|
| 12 |
+
--font-size-h2: 1.875rem;
|
| 13 |
+
--font-size-h3: 1.25rem;
|
| 14 |
+
--font-size-body: 1rem;
|
| 15 |
+
--font-size-editor: 1.125rem;
|
| 16 |
+
--font-size-caption: 0.875rem;
|
| 17 |
+
--font-size-label: 0.75rem;
|
| 18 |
+
|
| 19 |
+
--font-weight-regular: 400;
|
| 20 |
+
--font-weight-medium: 500;
|
| 21 |
+
--font-weight-semibold: 600;
|
| 22 |
+
--font-weight-bold: 700;
|
| 23 |
+
|
| 24 |
+
--line-height-editor: 1.9;
|
| 25 |
+
--line-height-body: 1.75;
|
| 26 |
+
--letter-spacing-arabic: 0.015em;
|
| 27 |
+
|
| 28 |
+
--spacing-xs: 0.25rem;
|
| 29 |
+
--spacing-sm: 0.5rem;
|
| 30 |
+
--spacing-md: 1rem;
|
| 31 |
+
--spacing-lg: 1.5rem;
|
| 32 |
+
--spacing-xl: 2rem;
|
| 33 |
+
--spacing-section: 6rem;
|
| 34 |
+
|
| 35 |
+
--radius-sm: 0.5rem;
|
| 36 |
+
--radius-md: 0.75rem;
|
| 37 |
+
--radius-lg: 1rem;
|
| 38 |
+
--radius-xl: 1.5rem;
|
| 39 |
+
--radius-card: 1rem;
|
| 40 |
+
|
| 41 |
+
--transition-fast: 0.15s ease;
|
| 42 |
+
--transition-base: 0.2s ease;
|
| 43 |
+
--transition-slow: 0.3s ease;
|
| 44 |
+
--transition-spring: 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* ── Dark Theme — soft charcoal, easy on eyes ── */
|
| 48 |
+
:root,
|
| 49 |
+
[data-theme="dark"] {
|
| 50 |
+
color-scheme: dark;
|
| 51 |
+
|
| 52 |
+
--color-bg: #12141A;
|
| 53 |
+
--color-surface: #1A1D26;
|
| 54 |
+
--color-surface-elevated: #242833;
|
| 55 |
+
--color-editor: #1E2229;
|
| 56 |
+
|
| 57 |
+
--color-primary: #6BA3E0;
|
| 58 |
+
--color-secondary: #A594E8;
|
| 59 |
+
--color-accent: #6BBECF;
|
| 60 |
+
|
| 61 |
+
--color-success: #6BC98A;
|
| 62 |
+
--color-warning: #E4B35A;
|
| 63 |
+
--color-error: #E88A8A;
|
| 64 |
+
|
| 65 |
+
--color-text-primary: #ECEEF2;
|
| 66 |
+
--color-text-secondary: #B4BBC6;
|
| 67 |
+
--color-text-muted: #8A939F;
|
| 68 |
+
--color-text-inverse: #F4F5F7;
|
| 69 |
+
--color-placeholder: #9AA3AE;
|
| 70 |
+
|
| 71 |
+
--color-border: rgba(236, 238, 242, 0.09);
|
| 72 |
+
--color-border-strong: rgba(236, 238, 242, 0.16);
|
| 73 |
+
|
| 74 |
+
--focus-ring: rgba(107, 163, 224, 0.42);
|
| 75 |
+
|
| 76 |
+
--shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.16);
|
| 77 |
+
--shadow-card: 0 8px 24px rgba(0, 0, 0, 0.28);
|
| 78 |
+
--shadow-popover: 0 16px 40px rgba(0, 0, 0, 0.38);
|
| 79 |
+
--shadow-editor: 0 2px 16px rgba(0, 0, 0, 0.2);
|
| 80 |
+
--shadow-glow: 0 0 20px rgba(107, 163, 224, 0.15);
|
| 81 |
+
|
| 82 |
+
--gradient-primary: linear-gradient(135deg, #6BA3E0, #A594E8);
|
| 83 |
+
--gradient-surface: linear-gradient(180deg, var(--color-surface), var(--color-bg));
|
| 84 |
+
|
| 85 |
+
--color-overlay: rgba(8, 10, 14, 0.62);
|
| 86 |
+
|
| 87 |
+
--highlight-spelling-bg: rgba(232, 138, 138, 0.16);
|
| 88 |
+
--highlight-spelling-border: #E88A8A;
|
| 89 |
+
--highlight-grammar-bg: rgba(228, 179, 90, 0.16);
|
| 90 |
+
--highlight-grammar-border: #E4B35A;
|
| 91 |
+
--highlight-punctuation-bg: rgba(107, 201, 138, 0.14);
|
| 92 |
+
--highlight-punctuation-border: #6BC98A;
|
| 93 |
+
|
| 94 |
+
--color-nav-bg: rgba(18, 20, 26, 0.94);
|
| 95 |
+
--color-badge-spelling-bg: rgba(232, 138, 138, 0.18);
|
| 96 |
+
--color-badge-grammar-bg: rgba(228, 179, 90, 0.18);
|
| 97 |
+
--color-badge-punctuation-bg: rgba(107, 201, 138, 0.16);
|
| 98 |
+
--color-summary-accent-bg: rgba(165, 148, 232, 0.14);
|
| 99 |
+
--color-summary-accent-border: rgba(165, 148, 232, 0.38);
|
| 100 |
+
--color-suggestion-hover-bg: rgba(107, 163, 224, 0.12);
|
| 101 |
+
--color-primary-subtle-bg: rgba(107, 163, 224, 0.14);
|
| 102 |
+
--color-primary-subtle-border: rgba(107, 163, 224, 0.32);
|
| 103 |
+
|
| 104 |
+
--primary-color: var(--color-primary);
|
| 105 |
+
--secondary-color: var(--color-secondary);
|
| 106 |
+
--text-color: var(--color-text-primary);
|
| 107 |
+
--text-secondary: var(--color-text-secondary);
|
| 108 |
+
--surface-color: var(--color-surface);
|
| 109 |
+
--background-color: var(--color-bg);
|
| 110 |
+
--success-color: var(--color-success);
|
| 111 |
+
--warning-color: var(--color-warning);
|
| 112 |
+
--error-color: var(--color-error);
|
| 113 |
+
--nav-bg: var(--color-nav-bg);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* ── Light Theme — warm paper, strong readable text ── */
|
| 117 |
+
[data-theme="light"] {
|
| 118 |
+
color-scheme: light;
|
| 119 |
+
|
| 120 |
+
--color-bg: #F3F1EC;
|
| 121 |
+
--color-surface: #FAF9F6;
|
| 122 |
+
--color-surface-elevated: #EFEBE4;
|
| 123 |
+
--color-editor: #FFFDF8;
|
| 124 |
+
|
| 125 |
+
--color-primary: #2B6CB8;
|
| 126 |
+
--color-secondary: #6B57A8;
|
| 127 |
+
--color-accent: #2A8F9E;
|
| 128 |
+
|
| 129 |
+
--color-success: #2F8554;
|
| 130 |
+
--color-warning: #B7791F;
|
| 131 |
+
--color-error: #C53030;
|
| 132 |
+
|
| 133 |
+
--color-text-primary: #1A1D21;
|
| 134 |
+
--color-text-secondary: #3A424E;
|
| 135 |
+
--color-text-muted: #5A6472;
|
| 136 |
+
--color-text-inverse: #FAFAF8;
|
| 137 |
+
--color-placeholder: #6B7580;
|
| 138 |
+
|
| 139 |
+
--color-border: rgba(26, 29, 33, 0.1);
|
| 140 |
+
--color-border-strong: rgba(26, 29, 33, 0.18);
|
| 141 |
+
|
| 142 |
+
--focus-ring: rgba(43, 108, 184, 0.32);
|
| 143 |
+
|
| 144 |
+
--shadow-xs: 0 1px 3px rgba(26, 29, 33, 0.05);
|
| 145 |
+
--shadow-card: 0 6px 20px rgba(26, 29, 33, 0.07);
|
| 146 |
+
--shadow-popover: 0 14px 36px rgba(26, 29, 33, 0.11);
|
| 147 |
+
--shadow-editor: 0 1px 8px rgba(26, 29, 33, 0.05);
|
| 148 |
+
--shadow-glow: 0 0 20px rgba(43, 108, 184, 0.1);
|
| 149 |
+
|
| 150 |
+
--gradient-primary: linear-gradient(135deg, #2B6CB8, #6B57A8);
|
| 151 |
+
--gradient-surface: linear-gradient(180deg, var(--color-surface), var(--color-bg));
|
| 152 |
+
|
| 153 |
+
--color-overlay: rgba(26, 29, 33, 0.38);
|
| 154 |
+
|
| 155 |
+
--highlight-spelling-bg: #FDECEC;
|
| 156 |
+
--highlight-spelling-border: #C53030;
|
| 157 |
+
--highlight-grammar-bg: #FEF6E4;
|
| 158 |
+
--highlight-grammar-border: #B7791F;
|
| 159 |
+
--highlight-punctuation-bg: #EAF6EE;
|
| 160 |
+
--highlight-punctuation-border: #2F8554;
|
| 161 |
+
|
| 162 |
+
--color-nav-bg: rgba(250, 249, 246, 0.94);
|
| 163 |
+
--color-badge-spelling-bg: rgba(197, 48, 48, 0.1);
|
| 164 |
+
--color-badge-grammar-bg: rgba(183, 121, 31, 0.12);
|
| 165 |
+
--color-badge-punctuation-bg: rgba(47, 133, 84, 0.1);
|
| 166 |
+
--color-summary-accent-bg: rgba(107, 87, 168, 0.08);
|
| 167 |
+
--color-summary-accent-border: rgba(107, 87, 168, 0.22);
|
| 168 |
+
--color-suggestion-hover-bg: rgba(43, 108, 184, 0.07);
|
| 169 |
+
--color-primary-subtle-bg: rgba(43, 108, 184, 0.09);
|
| 170 |
+
--color-primary-subtle-border: rgba(43, 108, 184, 0.22);
|
| 171 |
+
|
| 172 |
+
--primary-color: var(--color-primary);
|
| 173 |
+
--secondary-color: var(--color-secondary);
|
| 174 |
+
--text-color: var(--color-text-primary);
|
| 175 |
+
--text-secondary: var(--color-text-secondary);
|
| 176 |
+
--surface-color: var(--color-surface);
|
| 177 |
+
--background-color: var(--color-bg);
|
| 178 |
+
--success-color: var(--color-success);
|
| 179 |
+
--warning-color: var(--color-warning);
|
| 180 |
+
--error-color: var(--color-error);
|
| 181 |
+
--nav-bg: var(--color-nav-bg);
|
| 182 |
+
}
|
extension/shared/vendor/docx.umd.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
extension/shared/vendor/html2pdf.bundle.min.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
extension/shared/vendor/mammoth.browser.min.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
extension/sidepanel/sidepanel.css
CHANGED
|
@@ -1,43 +1,46 @@
|
|
| 1 |
/* ══════════════════════════════════════════════
|
| 2 |
Bayan Side Panel — Styles
|
| 3 |
Persistent workspace | RTL | Dark premium
|
|
|
|
| 4 |
══════════════════════════════════════════════ */
|
| 5 |
|
| 6 |
/* ── Design Tokens (shared with popup) ── */
|
| 7 |
:root {
|
| 8 |
-
--sp-primary: #
|
| 9 |
-
--sp-primary-light: #
|
| 10 |
-
--sp-primary-dark: #
|
| 11 |
-
--sp-primary-glow: rgba(
|
| 12 |
|
| 13 |
-
--sp-bg: #
|
| 14 |
-
--sp-surface: #
|
| 15 |
-
--sp-surface-hover: #
|
| 16 |
-
--sp-surface-active: #
|
| 17 |
-
--sp-border:
|
| 18 |
-
--sp-border-light:
|
| 19 |
|
| 20 |
-
--sp-text: #
|
| 21 |
-
--sp-text-secondary: #
|
| 22 |
-
--sp-text-muted: #
|
| 23 |
|
| 24 |
-
--sp-success: #
|
| 25 |
-
--sp-warning: #
|
| 26 |
-
--sp-error: #
|
| 27 |
|
| 28 |
-
--sp-spelling: #
|
| 29 |
-
--sp-grammar: #
|
| 30 |
-
--sp-punctuation: #6BC98A;
|
| 31 |
|
| 32 |
-
--sp-radius:
|
| 33 |
-
--sp-radius-sm:
|
| 34 |
-
--sp-radius-lg:
|
| 35 |
|
| 36 |
-
--sp-font:
|
| 37 |
-
--sp-font-arabic: '
|
| 38 |
|
| 39 |
-
--sp-transition:
|
| 40 |
-
--sp-spring:
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
/* ── Reset ── */
|
|
@@ -52,19 +55,18 @@ body {
|
|
| 52 |
min-height: 100vh;
|
| 53 |
overflow-y: auto;
|
| 54 |
overflow-x: hidden;
|
| 55 |
-
font-family: var(--
|
| 56 |
font-size: 13px;
|
| 57 |
line-height: 1.6;
|
| 58 |
-
color: var(--
|
| 59 |
-
background: var(--
|
| 60 |
direction: rtl;
|
| 61 |
-webkit-font-smoothing: antialiased;
|
| 62 |
}
|
| 63 |
|
| 64 |
-
/* Custom scrollbar */
|
| 65 |
::-webkit-scrollbar { width: 4px; }
|
| 66 |
::-webkit-scrollbar-track { background: transparent; }
|
| 67 |
-
::-webkit-scrollbar-thumb { background: var(--
|
| 68 |
|
| 69 |
/* ══════════════════════════════════════════════
|
| 70 |
Header
|
|
@@ -74,8 +76,8 @@ body {
|
|
| 74 |
align-items: center;
|
| 75 |
justify-content: space-between;
|
| 76 |
padding: 10px 14px;
|
| 77 |
-
background: linear-gradient(135deg, var(--
|
| 78 |
-
border-bottom: 1px solid var(--
|
| 79 |
position: sticky;
|
| 80 |
top: 0;
|
| 81 |
z-index: 100;
|
|
@@ -91,7 +93,7 @@ body {
|
|
| 91 |
.sp-header-title {
|
| 92 |
font-size: 16px;
|
| 93 |
font-weight: 700;
|
| 94 |
-
background: linear-gradient(135deg, var(--
|
| 95 |
-webkit-background-clip: text;
|
| 96 |
-webkit-text-fill-color: transparent;
|
| 97 |
letter-spacing: -0.5px;
|
|
@@ -123,31 +125,51 @@ body {
|
|
| 123 |
display: flex;
|
| 124 |
gap: 0;
|
| 125 |
padding: 0 14px;
|
| 126 |
-
background: var(--
|
| 127 |
-
border-bottom: 1px solid var(--
|
| 128 |
}
|
| 129 |
|
| 130 |
.sp-tab {
|
| 131 |
-
flex: 1;
|
| 132 |
display: flex;
|
| 133 |
align-items: center;
|
| 134 |
justify-content: center;
|
| 135 |
gap: 5px;
|
| 136 |
-
padding: 8px
|
| 137 |
border: none;
|
| 138 |
background: transparent;
|
| 139 |
color: var(--sp-text-muted);
|
| 140 |
-
font-family: var(--
|
| 141 |
font-size: 12px;
|
| 142 |
font-weight: 600;
|
| 143 |
cursor: pointer;
|
| 144 |
-
border-bottom: 2px solid transparent;
|
| 145 |
transition: all var(--sp-transition);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
}
|
| 147 |
-
|
|
|
|
| 148 |
.sp-tab.active {
|
| 149 |
-
color: var(--
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
| 151 |
}
|
| 152 |
|
| 153 |
/* ══════════════════════════════════════════════
|
|
@@ -172,14 +194,27 @@ body {
|
|
| 172 |
margin-bottom: 10px;
|
| 173 |
}
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
.sp-textarea {
|
| 176 |
width: 100%;
|
| 177 |
padding: 10px 12px;
|
| 178 |
-
border: 1px solid var(--
|
| 179 |
border-radius: var(--sp-radius);
|
| 180 |
-
background: var(--
|
| 181 |
-
color: var(--
|
| 182 |
-
font-family: var(--
|
| 183 |
font-size: 14px;
|
| 184 |
line-height: 1.7;
|
| 185 |
resize: vertical;
|
|
@@ -188,11 +223,11 @@ body {
|
|
| 188 |
}
|
| 189 |
.sp-textarea:focus {
|
| 190 |
outline: none;
|
| 191 |
-
border-color: var(--
|
| 192 |
box-shadow: 0 0 0 3px var(--sp-primary-glow);
|
| 193 |
}
|
| 194 |
.sp-textarea::placeholder {
|
| 195 |
-
color: var(--sp-text-muted);
|
| 196 |
font-size: 13px;
|
| 197 |
}
|
| 198 |
|
|
@@ -225,7 +260,7 @@ body {
|
|
| 225 |
padding: 7px 14px;
|
| 226 |
border: none;
|
| 227 |
border-radius: var(--sp-radius-sm);
|
| 228 |
-
font-family: var(--
|
| 229 |
font-size: 12px;
|
| 230 |
font-weight: 600;
|
| 231 |
cursor: pointer;
|
|
@@ -234,7 +269,7 @@ body {
|
|
| 234 |
|
| 235 |
.sp-btn-primary {
|
| 236 |
flex: 1;
|
| 237 |
-
background: linear-gradient(135deg, var(--
|
| 238 |
color: white;
|
| 239 |
box-shadow: 0 2px 8px var(--sp-primary-glow);
|
| 240 |
}
|
|
@@ -247,13 +282,29 @@ body {
|
|
| 247 |
}
|
| 248 |
|
| 249 |
.sp-btn-ghost {
|
| 250 |
-
background: var(--
|
| 251 |
-
color: var(--
|
| 252 |
-
border: 1px solid var(--
|
| 253 |
}
|
| 254 |
.sp-btn-ghost:hover {
|
| 255 |
-
background: var(--
|
| 256 |
-
color: var(--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
}
|
| 258 |
|
| 259 |
.sp-btn-icon {
|
|
@@ -264,42 +315,51 @@ body {
|
|
| 264 |
height: 28px;
|
| 265 |
border: none;
|
| 266 |
border-radius: var(--sp-radius-sm);
|
| 267 |
-
background: var(--
|
| 268 |
-
color: var(--
|
| 269 |
cursor: pointer;
|
| 270 |
transition: all var(--sp-transition);
|
| 271 |
}
|
| 272 |
.sp-btn-icon:hover {
|
| 273 |
-
background: var(--
|
| 274 |
-
color: var(--
|
| 275 |
}
|
| 276 |
|
| 277 |
/* ══════════════════════════════════════════════
|
| 278 |
-
Score
|
| 279 |
══════════════════════════════════════════════ */
|
| 280 |
.sp-score {
|
| 281 |
display: flex;
|
|
|
|
| 282 |
align-items: center;
|
| 283 |
-
|
| 284 |
-
padding: 12px;
|
| 285 |
margin-bottom: 12px;
|
| 286 |
-
background: var(--
|
| 287 |
-
border: 1px solid var(--
|
| 288 |
border-radius: var(--sp-radius);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
}
|
| 290 |
|
| 291 |
.sp-score-ring {
|
| 292 |
position: relative;
|
| 293 |
-
width:
|
| 294 |
-
height:
|
| 295 |
flex-shrink: 0;
|
|
|
|
| 296 |
}
|
| 297 |
.sp-score-ring svg {
|
| 298 |
-
|
| 299 |
-
height: 100%;
|
| 300 |
}
|
| 301 |
-
.sp-score-ring circle:last-
|
| 302 |
-
transition: stroke-dashoffset
|
| 303 |
}
|
| 304 |
|
| 305 |
.sp-score-value {
|
|
@@ -308,43 +368,49 @@ body {
|
|
| 308 |
display: flex;
|
| 309 |
align-items: center;
|
| 310 |
justify-content: center;
|
| 311 |
-
font-size:
|
| 312 |
-
font-weight:
|
| 313 |
-
color: var(--
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
.sp-score-details {
|
| 317 |
-
flex: 1;
|
| 318 |
-
min-width: 0;
|
| 319 |
}
|
| 320 |
|
| 321 |
.sp-score-hint {
|
| 322 |
display: block;
|
| 323 |
-
font-size:
|
| 324 |
-
color: var(--
|
| 325 |
-
margin-bottom:
|
| 326 |
}
|
| 327 |
|
| 328 |
.sp-score-counts {
|
| 329 |
display: flex;
|
| 330 |
gap: 10px;
|
| 331 |
flex-wrap: wrap;
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
color: var(--sp-text-muted);
|
|
|
|
|
|
|
|
|
|
| 334 |
}
|
| 335 |
-
.sp-count
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
| 338 |
|
| 339 |
/* ══════════════════════════════════════════════
|
| 340 |
Results
|
| 341 |
══════════════════════════════════════════════ */
|
| 342 |
.sp-result {
|
| 343 |
margin-bottom: 12px;
|
| 344 |
-
border: 1px solid var(--
|
| 345 |
border-radius: var(--sp-radius);
|
| 346 |
-
background: var(--
|
| 347 |
overflow: hidden;
|
|
|
|
| 348 |
}
|
| 349 |
|
| 350 |
.sp-result-header {
|
|
@@ -354,73 +420,31 @@ body {
|
|
| 354 |
padding: 8px 12px;
|
| 355 |
font-size: 11px;
|
| 356 |
font-weight: 700;
|
| 357 |
-
color: var(--
|
| 358 |
-
background: var(--
|
| 359 |
-
border-bottom: 1px solid var(--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
}
|
| 361 |
|
| 362 |
.sp-result-actions {
|
| 363 |
display: flex;
|
| 364 |
-
|
|
|
|
| 365 |
}
|
| 366 |
|
| 367 |
.sp-result-text {
|
| 368 |
padding: 10px 12px;
|
| 369 |
font-size: 14px;
|
| 370 |
line-height: 1.8;
|
| 371 |
-
color: var(--
|
| 372 |
word-break: break-word;
|
| 373 |
}
|
| 374 |
|
| 375 |
-
.sp-summary-meta {
|
| 376 |
-
padding: 6px 12px 10px;
|
| 377 |
-
font-size: 10px;
|
| 378 |
-
color: var(--sp-text-muted);
|
| 379 |
-
}
|
| 380 |
-
|
| 381 |
-
/* ══════════════════════════════════════════════
|
| 382 |
-
Quran verse translation
|
| 383 |
-
══════════════════════════════════════════════ */
|
| 384 |
-
.sp-quran-translate {
|
| 385 |
-
padding: 10px 12px 12px;
|
| 386 |
-
border-top: 1px solid var(--sp-border);
|
| 387 |
-
}
|
| 388 |
-
|
| 389 |
-
.sp-quran-translate-row {
|
| 390 |
-
display: flex;
|
| 391 |
-
align-items: center;
|
| 392 |
-
gap: 8px;
|
| 393 |
-
flex-wrap: wrap;
|
| 394 |
-
}
|
| 395 |
-
|
| 396 |
-
.sp-quran-translate-label {
|
| 397 |
-
font-size: 12px;
|
| 398 |
-
font-weight: 600;
|
| 399 |
-
color: var(--sp-text-secondary);
|
| 400 |
-
}
|
| 401 |
-
|
| 402 |
-
.sp-select {
|
| 403 |
-
flex: 1;
|
| 404 |
-
min-width: 140px;
|
| 405 |
-
padding: 6px 10px;
|
| 406 |
-
font-size: 13px;
|
| 407 |
-
color: var(--sp-text);
|
| 408 |
-
background: var(--sp-surface);
|
| 409 |
-
border: 1px solid var(--sp-border);
|
| 410 |
-
border-radius: 8px;
|
| 411 |
-
cursor: pointer;
|
| 412 |
-
}
|
| 413 |
-
|
| 414 |
-
.sp-select:focus {
|
| 415 |
-
outline: none;
|
| 416 |
-
border-color: var(--sp-primary);
|
| 417 |
-
}
|
| 418 |
-
|
| 419 |
-
.sp-select option {
|
| 420 |
-
background: var(--sp-surface);
|
| 421 |
-
color: var(--sp-text);
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
/* ══════════════════════════════════════════════
|
| 425 |
Highlighted text (from bayan-renderer.js)
|
| 426 |
══════════════════════════════════════════════ */
|
|
@@ -451,14 +475,20 @@ body {
|
|
| 451 |
══════════════════════════════════════════════ */
|
| 452 |
.sp-suggestions {
|
| 453 |
margin-bottom: 12px;
|
|
|
|
| 454 |
}
|
| 455 |
|
| 456 |
.sp-suggestions-header {
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
margin-bottom:
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
}
|
| 463 |
|
| 464 |
.sp-suggestions-list {
|
|
@@ -467,31 +497,17 @@ body {
|
|
| 467 |
gap: 6px;
|
| 468 |
}
|
| 469 |
|
| 470 |
-
/* Autocomplete suggestion chips (إكمال tab) — horizontal wrap */
|
| 471 |
-
#autocomplete-list {
|
| 472 |
-
flex-direction: row;
|
| 473 |
-
flex-wrap: wrap;
|
| 474 |
-
}
|
| 475 |
-
.sp-ac-chip {
|
| 476 |
-
cursor: pointer;
|
| 477 |
-
}
|
| 478 |
-
.sp-ac-empty {
|
| 479 |
-
color: var(--sp-text-muted, #94a3b8);
|
| 480 |
-
font-size: 13px;
|
| 481 |
-
padding: 4px 2px;
|
| 482 |
-
}
|
| 483 |
-
|
| 484 |
/* Suggestion card — reuses bayan-ui.js HTML classes */
|
| 485 |
.bayan-suggestion-card {
|
| 486 |
padding: 8px 10px;
|
| 487 |
-
background: var(--
|
| 488 |
-
border: 1px solid var(--
|
| 489 |
border-radius: var(--sp-radius-sm);
|
| 490 |
transition: border-color var(--sp-transition), background var(--sp-transition);
|
| 491 |
}
|
| 492 |
.bayan-suggestion-card:hover {
|
| 493 |
border-color: var(--sp-border-light);
|
| 494 |
-
background: var(--
|
| 495 |
}
|
| 496 |
|
| 497 |
.bayan-suggestion-badge {
|
|
@@ -538,26 +554,26 @@ body {
|
|
| 538 |
|
| 539 |
.bayan-alt-chip {
|
| 540 |
padding: 3px 8px;
|
| 541 |
-
border: 1px solid var(--
|
| 542 |
border-radius: 12px;
|
| 543 |
-
background: var(--
|
| 544 |
-
color: var(--
|
| 545 |
-
font-family: var(--
|
| 546 |
font-size: 11px;
|
| 547 |
cursor: pointer;
|
| 548 |
transition: all var(--sp-transition);
|
| 549 |
}
|
| 550 |
.bayan-alt-chip:hover {
|
| 551 |
-
border-color: var(--
|
| 552 |
background: var(--sp-primary-glow);
|
| 553 |
}
|
| 554 |
.bayan-alt-chip--main {
|
| 555 |
-
border-color: var(--
|
| 556 |
background: rgba(99, 102, 241, 0.1);
|
| 557 |
-
color: var(--
|
| 558 |
}
|
| 559 |
.bayan-alt-chip--keep {
|
| 560 |
-
border-color: var(--
|
| 561 |
color: var(--sp-text-muted);
|
| 562 |
font-size: 10px;
|
| 563 |
}
|
|
@@ -567,12 +583,176 @@ body {
|
|
| 567 |
}
|
| 568 |
|
| 569 |
/* ══════════════════════════════════════════════
|
| 570 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
══════════════════════════════════════════════ */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
.sp-radio-group {
|
| 573 |
display: flex;
|
| 574 |
gap: 12px;
|
| 575 |
-
margin-
|
| 576 |
padding: 0 2px;
|
| 577 |
}
|
| 578 |
|
|
@@ -581,22 +761,105 @@ body {
|
|
| 581 |
align-items: center;
|
| 582 |
gap: 4px;
|
| 583 |
font-size: 11px;
|
| 584 |
-
color: var(--
|
| 585 |
cursor: pointer;
|
| 586 |
}
|
| 587 |
.sp-radio input[type="radio"] {
|
| 588 |
-
accent-color: var(--
|
| 589 |
width: 13px;
|
| 590 |
height: 13px;
|
| 591 |
}
|
| 592 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
/* ══════════════════════════════════════════════
|
| 594 |
Timing
|
| 595 |
══════════════════════════════════════════════ */
|
| 596 |
.sp-timing {
|
| 597 |
padding: 6px 10px;
|
| 598 |
margin-bottom: 8px;
|
| 599 |
-
background: var(--
|
| 600 |
border-radius: var(--sp-radius-sm);
|
| 601 |
font-size: 10px;
|
| 602 |
color: var(--sp-text-muted);
|
|
@@ -615,17 +878,17 @@ body {
|
|
| 615 |
align-items: center;
|
| 616 |
justify-content: center;
|
| 617 |
gap: 12px;
|
| 618 |
-
background: rgba(15, 15, 20, 0.85);
|
| 619 |
backdrop-filter: blur(6px);
|
| 620 |
-
color: var(--
|
| 621 |
font-size: 13px;
|
| 622 |
}
|
| 623 |
|
| 624 |
.sp-spinner {
|
| 625 |
width: 32px;
|
| 626 |
height: 32px;
|
| 627 |
-
border: 3px solid var(--
|
| 628 |
-
border-top-color: var(--
|
| 629 |
border-radius: 50%;
|
| 630 |
animation: spSpin 800ms linear infinite;
|
| 631 |
}
|
|
@@ -643,10 +906,10 @@ body {
|
|
| 643 |
left: 50%;
|
| 644 |
transform: translateX(-50%) translateY(60px);
|
| 645 |
padding: 8px 16px;
|
| 646 |
-
background: var(--
|
| 647 |
-
border: 1px solid var(--
|
| 648 |
border-radius: 20px;
|
| 649 |
-
color: var(--
|
| 650 |
font-size: 12px;
|
| 651 |
font-weight: 600;
|
| 652 |
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
@@ -671,6 +934,22 @@ body {
|
|
| 671 |
filter: grayscale(30%);
|
| 672 |
transition: opacity var(--sp-transition), filter var(--sp-transition);
|
| 673 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 674 |
|
| 675 |
/* ══════════════════════════════════════════════
|
| 676 |
Utilities
|
|
@@ -679,22 +958,6 @@ body {
|
|
| 679 |
display: none !important;
|
| 680 |
}
|
| 681 |
|
| 682 |
-
|
| 683 |
-
/* Light Theme Variables */
|
| 684 |
-
[data-theme="light"] {
|
| 685 |
-
--bayan-bg: #f9fafb;
|
| 686 |
-
--bayan-surface: #ffffff;
|
| 687 |
-
--bayan-surface-hover: #f3f4f6;
|
| 688 |
-
--bayan-surface-active: #e5e7eb;
|
| 689 |
-
--bayan-border: #e5e7eb;
|
| 690 |
-
--bayan-border-light: #d1d5db;
|
| 691 |
-
--bayan-text: #111827;
|
| 692 |
-
--bayan-text-secondary: #4b5563;
|
| 693 |
-
--bayan-text-muted: #9ca3af;
|
| 694 |
-
--bayan-success: #16a34a;
|
| 695 |
-
--bayan-warning: #d97706;
|
| 696 |
-
}
|
| 697 |
-
|
| 698 |
/* Theme Toggle Button Styles */
|
| 699 |
.theme-toggle-animated {
|
| 700 |
display: flex;
|
|
@@ -704,8 +967,8 @@ body {
|
|
| 704 |
height: 32px;
|
| 705 |
border: none;
|
| 706 |
border-radius: 50%;
|
| 707 |
-
background: var(--
|
| 708 |
-
color: var(--
|
| 709 |
cursor: pointer;
|
| 710 |
transition: background 0.3s ease, transform 0.3s ease, color 0.3s ease;
|
| 711 |
position: relative;
|
|
@@ -714,7 +977,7 @@ body {
|
|
| 714 |
}
|
| 715 |
|
| 716 |
.theme-toggle-animated:hover {
|
| 717 |
-
background: var(--
|
| 718 |
color: #fff;
|
| 719 |
transform: rotate(15deg);
|
| 720 |
}
|
|
@@ -743,21 +1006,3 @@ body {
|
|
| 743 |
transform: rotate(0) scale(1);
|
| 744 |
opacity: 1;
|
| 745 |
}
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
/* Light Theme Variables for Side Panel */
|
| 750 |
-
[data-theme="light"] {
|
| 751 |
-
--sp-bg: #f9fafb;
|
| 752 |
-
--sp-surface: #ffffff;
|
| 753 |
-
--sp-surface-hover: #f3f4f6;
|
| 754 |
-
--sp-surface-active: #e5e7eb;
|
| 755 |
-
--sp-border: #e5e7eb;
|
| 756 |
-
--sp-border-light: #d1d5db;
|
| 757 |
-
--sp-text: #111827;
|
| 758 |
-
--sp-text-secondary: #4b5563;
|
| 759 |
-
--sp-text-muted: #9ca3af;
|
| 760 |
-
--sp-success: #16a34a;
|
| 761 |
-
--sp-warning: #d97706;
|
| 762 |
-
}
|
| 763 |
-
|
|
|
|
| 1 |
/* ══════════════════════════════════════════════
|
| 2 |
Bayan Side Panel — Styles
|
| 3 |
Persistent workspace | RTL | Dark premium
|
| 4 |
+
Matches popup design system
|
| 5 |
══════════════════════════════════════════════ */
|
| 6 |
|
| 7 |
/* ── Design Tokens (shared with popup) ── */
|
| 8 |
:root {
|
| 9 |
+
--sp-primary: var(--color-primary, #6BA3E0);
|
| 10 |
+
--sp-primary-light: var(--color-secondary, #A594E8);
|
| 11 |
+
--sp-primary-dark: var(--color-primary, #6BA3E0);
|
| 12 |
+
--sp-primary-glow: var(--focus-ring, rgba(107, 163, 224, 0.42));
|
| 13 |
|
| 14 |
+
--sp-bg: var(--color-bg, #12141A);
|
| 15 |
+
--sp-surface: var(--color-surface, #1A1D26);
|
| 16 |
+
--sp-surface-hover: var(--color-surface-elevated, #242833);
|
| 17 |
+
--sp-surface-active: var(--color-surface-elevated, #242833);
|
| 18 |
+
--sp-border: var(--color-border, rgba(236, 238, 242, 0.09));
|
| 19 |
+
--sp-border-light: var(--color-border-strong, rgba(236, 238, 242, 0.16));
|
| 20 |
|
| 21 |
+
--sp-text: var(--color-text-primary, #ECEEF2);
|
| 22 |
+
--sp-text-secondary: var(--color-text-secondary, #B4BBC6);
|
| 23 |
+
--sp-text-muted: var(--color-text-muted, #8A939F);
|
| 24 |
|
| 25 |
+
--sp-success: var(--color-success, #6BC98A);
|
| 26 |
+
--sp-warning: var(--color-warning, #E4B35A);
|
| 27 |
+
--sp-error: var(--color-error, #E88A8A);
|
| 28 |
|
| 29 |
+
--sp-spelling: var(--color-error, #E88A8A);
|
| 30 |
+
--sp-grammar: var(--color-warning, #E4B35A);
|
| 31 |
+
--sp-punctuation: var(--color-success, #6BC98A);
|
| 32 |
|
| 33 |
+
--sp-radius: var(--radius-md, 0.75rem);
|
| 34 |
+
--sp-radius-sm: var(--radius-sm, 0.5rem);
|
| 35 |
+
--sp-radius-lg: var(--radius-lg, 1rem);
|
| 36 |
|
| 37 |
+
--sp-font: var(--font-family-primary, 'Cairo', 'Tajawal', sans-serif);
|
| 38 |
+
--sp-font-arabic: var(--font-family-primary, 'Cairo', 'Tajawal', 'Noto Sans Arabic', sans-serif);
|
| 39 |
|
| 40 |
+
--sp-transition: var(--transition-base, 0.2s ease);
|
| 41 |
+
--sp-spring: var(--transition-spring, 0.4s cubic-bezier(0.34, 1.56, 0.64, 1));
|
| 42 |
+
|
| 43 |
+
--color-surface-hover: var(--color-surface-elevated, #242833);
|
| 44 |
}
|
| 45 |
|
| 46 |
/* ── Reset ── */
|
|
|
|
| 55 |
min-height: 100vh;
|
| 56 |
overflow-y: auto;
|
| 57 |
overflow-x: hidden;
|
| 58 |
+
font-family: var(--font-family-primary, 'Cairo', 'Tajawal', sans-serif);
|
| 59 |
font-size: 13px;
|
| 60 |
line-height: 1.6;
|
| 61 |
+
color: var(--text-color);
|
| 62 |
+
background: var(--color-bg);
|
| 63 |
direction: rtl;
|
| 64 |
-webkit-font-smoothing: antialiased;
|
| 65 |
}
|
| 66 |
|
|
|
|
| 67 |
::-webkit-scrollbar { width: 4px; }
|
| 68 |
::-webkit-scrollbar-track { background: transparent; }
|
| 69 |
+
::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 4px; }
|
| 70 |
|
| 71 |
/* ══════════════════════════════════════════════
|
| 72 |
Header
|
|
|
|
| 76 |
align-items: center;
|
| 77 |
justify-content: space-between;
|
| 78 |
padding: 10px 14px;
|
| 79 |
+
background: linear-gradient(135deg, var(--color-surface) 0%, rgba(99, 102, 241, 0.08) 100%);
|
| 80 |
+
border-bottom: 1px solid var(--color-border);
|
| 81 |
position: sticky;
|
| 82 |
top: 0;
|
| 83 |
z-index: 100;
|
|
|
|
| 93 |
.sp-header-title {
|
| 94 |
font-size: 16px;
|
| 95 |
font-weight: 700;
|
| 96 |
+
background: linear-gradient(135deg, var(--secondary-color), #a78bfa);
|
| 97 |
-webkit-background-clip: text;
|
| 98 |
-webkit-text-fill-color: transparent;
|
| 99 |
letter-spacing: -0.5px;
|
|
|
|
| 125 |
display: flex;
|
| 126 |
gap: 0;
|
| 127 |
padding: 0 14px;
|
| 128 |
+
background: var(--color-surface);
|
| 129 |
+
border-bottom: 1px solid var(--color-border);
|
| 130 |
}
|
| 131 |
|
| 132 |
.sp-tab {
|
| 133 |
+
flex: 1 1 auto;
|
| 134 |
display: flex;
|
| 135 |
align-items: center;
|
| 136 |
justify-content: center;
|
| 137 |
gap: 5px;
|
| 138 |
+
padding: 8px 6px;
|
| 139 |
border: none;
|
| 140 |
background: transparent;
|
| 141 |
color: var(--sp-text-muted);
|
| 142 |
+
font-family: var(--font-family-primary, 'Cairo', 'Tajawal', sans-serif);
|
| 143 |
font-size: 12px;
|
| 144 |
font-weight: 600;
|
| 145 |
cursor: pointer;
|
|
|
|
| 146 |
transition: all var(--sp-transition);
|
| 147 |
+
position: relative;
|
| 148 |
+
white-space: nowrap;
|
| 149 |
+
min-width: 0;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.sp-tab::after {
|
| 153 |
+
content: '';
|
| 154 |
+
position: absolute;
|
| 155 |
+
bottom: 0;
|
| 156 |
+
left: 20%;
|
| 157 |
+
right: 20%;
|
| 158 |
+
height: 2px;
|
| 159 |
+
background: var(--primary-color);
|
| 160 |
+
border-radius: 2px 2px 0 0;
|
| 161 |
+
opacity: 0;
|
| 162 |
+
transform: scaleX(0);
|
| 163 |
+
transition: all var(--sp-spring);
|
| 164 |
}
|
| 165 |
+
|
| 166 |
+
.sp-tab:hover { color: var(--text-color); background: var(--color-surface-hover); }
|
| 167 |
.sp-tab.active {
|
| 168 |
+
color: var(--secondary-color);
|
| 169 |
+
}
|
| 170 |
+
.sp-tab.active::after {
|
| 171 |
+
opacity: 1;
|
| 172 |
+
transform: scaleX(1);
|
| 173 |
}
|
| 174 |
|
| 175 |
/* ══════════════════════════════════════════════
|
|
|
|
| 194 |
margin-bottom: 10px;
|
| 195 |
}
|
| 196 |
|
| 197 |
+
.sp-label-row {
|
| 198 |
+
display: flex;
|
| 199 |
+
align-items: center;
|
| 200 |
+
justify-content: space-between;
|
| 201 |
+
margin-bottom: 6px;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.sp-label {
|
| 205 |
+
font-size: 12px;
|
| 206 |
+
font-weight: 600;
|
| 207 |
+
color: var(--text-secondary);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
.sp-textarea {
|
| 211 |
width: 100%;
|
| 212 |
padding: 10px 12px;
|
| 213 |
+
border: 1px solid var(--color-border);
|
| 214 |
border-radius: var(--sp-radius);
|
| 215 |
+
background: var(--color-surface);
|
| 216 |
+
color: var(--text-color);
|
| 217 |
+
font-family: var(--font-family-primary, 'Cairo', 'Tajawal', sans-serif);
|
| 218 |
font-size: 14px;
|
| 219 |
line-height: 1.7;
|
| 220 |
resize: vertical;
|
|
|
|
| 223 |
}
|
| 224 |
.sp-textarea:focus {
|
| 225 |
outline: none;
|
| 226 |
+
border-color: var(--primary-color);
|
| 227 |
box-shadow: 0 0 0 3px var(--sp-primary-glow);
|
| 228 |
}
|
| 229 |
.sp-textarea::placeholder {
|
| 230 |
+
color: var(--color-placeholder, var(--sp-text-muted));
|
| 231 |
font-size: 13px;
|
| 232 |
}
|
| 233 |
|
|
|
|
| 260 |
padding: 7px 14px;
|
| 261 |
border: none;
|
| 262 |
border-radius: var(--sp-radius-sm);
|
| 263 |
+
font-family: var(--font-family-primary, 'Cairo', 'Tajawal', sans-serif);
|
| 264 |
font-size: 12px;
|
| 265 |
font-weight: 600;
|
| 266 |
cursor: pointer;
|
|
|
|
| 269 |
|
| 270 |
.sp-btn-primary {
|
| 271 |
flex: 1;
|
| 272 |
+
background: linear-gradient(135deg, var(--color-primary, #6BA3E0), var(--color-secondary, #A594E8));
|
| 273 |
color: white;
|
| 274 |
box-shadow: 0 2px 8px var(--sp-primary-glow);
|
| 275 |
}
|
|
|
|
| 282 |
}
|
| 283 |
|
| 284 |
.sp-btn-ghost {
|
| 285 |
+
background: var(--color-surface);
|
| 286 |
+
color: var(--text-secondary);
|
| 287 |
+
border: 1px solid var(--color-border);
|
| 288 |
}
|
| 289 |
.sp-btn-ghost:hover {
|
| 290 |
+
background: var(--color-surface-hover);
|
| 291 |
+
color: var(--text-color);
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.sp-btn-sm {
|
| 295 |
+
padding: 5px 12px;
|
| 296 |
+
font-size: 11px;
|
| 297 |
+
background: var(--primary-color);
|
| 298 |
+
color: white;
|
| 299 |
+
border: none;
|
| 300 |
+
border-radius: var(--sp-radius-sm);
|
| 301 |
+
font-family: var(--font-family-primary, 'Cairo', 'Tajawal', sans-serif);
|
| 302 |
+
font-weight: 600;
|
| 303 |
+
cursor: pointer;
|
| 304 |
+
transition: all var(--sp-transition);
|
| 305 |
+
}
|
| 306 |
+
.sp-btn-sm:hover {
|
| 307 |
+
background: var(--sp-primary-dark);
|
| 308 |
}
|
| 309 |
|
| 310 |
.sp-btn-icon {
|
|
|
|
| 315 |
height: 28px;
|
| 316 |
border: none;
|
| 317 |
border-radius: var(--sp-radius-sm);
|
| 318 |
+
background: var(--color-surface);
|
| 319 |
+
color: var(--text-secondary);
|
| 320 |
cursor: pointer;
|
| 321 |
transition: all var(--sp-transition);
|
| 322 |
}
|
| 323 |
.sp-btn-icon:hover {
|
| 324 |
+
background: var(--color-surface-hover);
|
| 325 |
+
color: var(--secondary-color);
|
| 326 |
}
|
| 327 |
|
| 328 |
/* ══════════════════════════════════════════════
|
| 329 |
+
Score (matching popup — vertical centered with gradient)
|
| 330 |
══════════════════════════════════════════════ */
|
| 331 |
.sp-score {
|
| 332 |
display: flex;
|
| 333 |
+
flex-direction: column;
|
| 334 |
align-items: center;
|
| 335 |
+
text-align: center;
|
| 336 |
+
padding: 16px 12px;
|
| 337 |
margin-bottom: 12px;
|
| 338 |
+
background: var(--color-surface);
|
| 339 |
+
border: 1px solid var(--color-border);
|
| 340 |
border-radius: var(--sp-radius);
|
| 341 |
+
animation: spFadeIn 300ms ease;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.sp-score-title {
|
| 345 |
+
font-size: 13px;
|
| 346 |
+
font-weight: 600;
|
| 347 |
+
color: var(--text-secondary);
|
| 348 |
+
margin-bottom: 10px;
|
| 349 |
}
|
| 350 |
|
| 351 |
.sp-score-ring {
|
| 352 |
position: relative;
|
| 353 |
+
width: 120px;
|
| 354 |
+
height: 120px;
|
| 355 |
flex-shrink: 0;
|
| 356 |
+
margin-bottom: 8px;
|
| 357 |
}
|
| 358 |
.sp-score-ring svg {
|
| 359 |
+
display: block;
|
|
|
|
| 360 |
}
|
| 361 |
+
.sp-score-ring circle:last-child {
|
| 362 |
+
transition: stroke-dashoffset 600ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 363 |
}
|
| 364 |
|
| 365 |
.sp-score-value {
|
|
|
|
| 368 |
display: flex;
|
| 369 |
align-items: center;
|
| 370 |
justify-content: center;
|
| 371 |
+
font-size: 28px;
|
| 372 |
+
font-weight: 700;
|
| 373 |
+
color: var(--secondary-color);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
}
|
| 375 |
|
| 376 |
.sp-score-hint {
|
| 377 |
display: block;
|
| 378 |
+
font-size: 12px;
|
| 379 |
+
color: var(--text-secondary);
|
| 380 |
+
margin-bottom: 8px;
|
| 381 |
}
|
| 382 |
|
| 383 |
.sp-score-counts {
|
| 384 |
display: flex;
|
| 385 |
gap: 10px;
|
| 386 |
flex-wrap: wrap;
|
| 387 |
+
justify-content: center;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.sp-count {
|
| 391 |
+
font-size: 11px;
|
| 392 |
color: var(--sp-text-muted);
|
| 393 |
+
display: flex;
|
| 394 |
+
align-items: center;
|
| 395 |
+
gap: 3px;
|
| 396 |
}
|
| 397 |
+
.sp-count span {
|
| 398 |
+
font-weight: 700;
|
| 399 |
+
}
|
| 400 |
+
.sp-count-spelling span { color: var(--sp-spelling); }
|
| 401 |
+
.sp-count-grammar span { color: var(--sp-grammar); }
|
| 402 |
+
.sp-count-punctuation span { color: var(--sp-punctuation); }
|
| 403 |
|
| 404 |
/* ══════════════════════════════════════════════
|
| 405 |
Results
|
| 406 |
══════════════════════════════════════════════ */
|
| 407 |
.sp-result {
|
| 408 |
margin-bottom: 12px;
|
| 409 |
+
border: 1px solid var(--color-border);
|
| 410 |
border-radius: var(--sp-radius);
|
| 411 |
+
background: var(--color-surface);
|
| 412 |
overflow: hidden;
|
| 413 |
+
animation: spFadeIn 300ms ease;
|
| 414 |
}
|
| 415 |
|
| 416 |
.sp-result-header {
|
|
|
|
| 420 |
padding: 8px 12px;
|
| 421 |
font-size: 11px;
|
| 422 |
font-weight: 700;
|
| 423 |
+
color: var(--text-secondary);
|
| 424 |
+
background: var(--color-surface-hover);
|
| 425 |
+
border-bottom: 1px solid var(--color-border);
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.sp-result-title {
|
| 429 |
+
font-size: 12px;
|
| 430 |
+
font-weight: 600;
|
| 431 |
+
color: var(--text-secondary);
|
| 432 |
}
|
| 433 |
|
| 434 |
.sp-result-actions {
|
| 435 |
display: flex;
|
| 436 |
+
align-items: center;
|
| 437 |
+
gap: 2px;
|
| 438 |
}
|
| 439 |
|
| 440 |
.sp-result-text {
|
| 441 |
padding: 10px 12px;
|
| 442 |
font-size: 14px;
|
| 443 |
line-height: 1.8;
|
| 444 |
+
color: var(--text-color);
|
| 445 |
word-break: break-word;
|
| 446 |
}
|
| 447 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
/* ══════════════════════════════════════════════
|
| 449 |
Highlighted text (from bayan-renderer.js)
|
| 450 |
══════════════════════════════════════════════ */
|
|
|
|
| 475 |
══════════════════════════════════════════════ */
|
| 476 |
.sp-suggestions {
|
| 477 |
margin-bottom: 12px;
|
| 478 |
+
animation: spFadeIn 300ms ease;
|
| 479 |
}
|
| 480 |
|
| 481 |
.sp-suggestions-header {
|
| 482 |
+
display: flex;
|
| 483 |
+
align-items: center;
|
| 484 |
+
justify-content: space-between;
|
| 485 |
+
margin-bottom: 8px;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
.sp-suggestions-title {
|
| 489 |
+
font-size: 12px;
|
| 490 |
+
font-weight: 600;
|
| 491 |
+
color: var(--text-secondary);
|
| 492 |
}
|
| 493 |
|
| 494 |
.sp-suggestions-list {
|
|
|
|
| 497 |
gap: 6px;
|
| 498 |
}
|
| 499 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
/* Suggestion card — reuses bayan-ui.js HTML classes */
|
| 501 |
.bayan-suggestion-card {
|
| 502 |
padding: 8px 10px;
|
| 503 |
+
background: var(--color-surface);
|
| 504 |
+
border: 1px solid var(--color-border);
|
| 505 |
border-radius: var(--sp-radius-sm);
|
| 506 |
transition: border-color var(--sp-transition), background var(--sp-transition);
|
| 507 |
}
|
| 508 |
.bayan-suggestion-card:hover {
|
| 509 |
border-color: var(--sp-border-light);
|
| 510 |
+
background: var(--color-surface-hover);
|
| 511 |
}
|
| 512 |
|
| 513 |
.bayan-suggestion-badge {
|
|
|
|
| 554 |
|
| 555 |
.bayan-alt-chip {
|
| 556 |
padding: 3px 8px;
|
| 557 |
+
border: 1px solid var(--color-border);
|
| 558 |
border-radius: 12px;
|
| 559 |
+
background: var(--color-surface-hover);
|
| 560 |
+
color: var(--text-color);
|
| 561 |
+
font-family: var(--font-family-primary, 'Cairo', 'Tajawal', sans-serif);
|
| 562 |
font-size: 11px;
|
| 563 |
cursor: pointer;
|
| 564 |
transition: all var(--sp-transition);
|
| 565 |
}
|
| 566 |
.bayan-alt-chip:hover {
|
| 567 |
+
border-color: var(--primary-color);
|
| 568 |
background: var(--sp-primary-glow);
|
| 569 |
}
|
| 570 |
.bayan-alt-chip--main {
|
| 571 |
+
border-color: var(--primary-color);
|
| 572 |
background: rgba(99, 102, 241, 0.1);
|
| 573 |
+
color: var(--secondary-color);
|
| 574 |
}
|
| 575 |
.bayan-alt-chip--keep {
|
| 576 |
+
border-color: var(--color-border);
|
| 577 |
color: var(--sp-text-muted);
|
| 578 |
font-size: 10px;
|
| 579 |
}
|
|
|
|
| 583 |
}
|
| 584 |
|
| 585 |
/* ══════════════════════════════════════════════
|
| 586 |
+
Summary Mode Toggle (paragraph / bullets)
|
| 587 |
+
══════════════════════════════════════════════ */
|
| 588 |
+
.sp-mode-toggle {
|
| 589 |
+
display: flex;
|
| 590 |
+
gap: 0;
|
| 591 |
+
margin-bottom: 12px;
|
| 592 |
+
border: 1px solid var(--color-border);
|
| 593 |
+
border-radius: var(--sp-radius-sm);
|
| 594 |
+
overflow: hidden;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.sp-mode-btn {
|
| 598 |
+
flex: 1;
|
| 599 |
+
display: flex;
|
| 600 |
+
align-items: center;
|
| 601 |
+
justify-content: center;
|
| 602 |
+
gap: 5px;
|
| 603 |
+
padding: 7px 10px;
|
| 604 |
+
border: none;
|
| 605 |
+
background: transparent;
|
| 606 |
+
color: var(--sp-text-muted);
|
| 607 |
+
font-family: var(--font-family-primary, 'Cairo', 'Tajawal', sans-serif);
|
| 608 |
+
font-size: 12px;
|
| 609 |
+
font-weight: 500;
|
| 610 |
+
cursor: pointer;
|
| 611 |
+
transition: all var(--sp-transition);
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.sp-mode-btn + .sp-mode-btn {
|
| 615 |
+
border-right: 1px solid var(--color-border);
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
.sp-mode-btn:hover {
|
| 619 |
+
background: var(--color-surface-hover);
|
| 620 |
+
color: var(--text-color);
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
.sp-mode-btn.active {
|
| 624 |
+
background: var(--primary-color);
|
| 625 |
+
color: white;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
/* ══════════════════════════════════════════════
|
| 629 |
+
Summary Stats
|
| 630 |
+
══════════════════════════════════════════════ */
|
| 631 |
+
.sp-summary-stats {
|
| 632 |
+
display: flex;
|
| 633 |
+
gap: 16px;
|
| 634 |
+
padding: 8px 12px;
|
| 635 |
+
border-bottom: 1px solid var(--color-border);
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
.sp-summary-stat {
|
| 639 |
+
font-size: 11px;
|
| 640 |
+
color: var(--sp-text-muted);
|
| 641 |
+
display: flex;
|
| 642 |
+
gap: 4px;
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
.sp-summary-stat-value {
|
| 646 |
+
font-weight: 700;
|
| 647 |
+
color: var(--text-secondary);
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
/* ══════════════════════════════════════════════
|
| 651 |
+
Description paragraph (dialect panel)
|
| 652 |
+
══════════════════════════════════════════════ */
|
| 653 |
+
.sp-description {
|
| 654 |
+
font-size: 11px;
|
| 655 |
+
color: var(--sp-text-muted);
|
| 656 |
+
margin-bottom: 8px;
|
| 657 |
+
line-height: 1.6;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
/* ══════════════════════════════════════════════
|
| 661 |
+
Input meta row (word count + import)
|
| 662 |
+
══════════════════════════════════════════════ */
|
| 663 |
+
.sp-input-meta-row {
|
| 664 |
+
display: flex;
|
| 665 |
+
align-items: center;
|
| 666 |
+
justify-content: space-between;
|
| 667 |
+
margin-top: 6px;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
.sp-input-meta {
|
| 671 |
+
font-size: 11px;
|
| 672 |
+
color: var(--sp-text-muted);
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.sp-sr-only {
|
| 676 |
+
position: absolute;
|
| 677 |
+
width: 1px;
|
| 678 |
+
height: 1px;
|
| 679 |
+
padding: 0;
|
| 680 |
+
margin: -1px;
|
| 681 |
+
overflow: hidden;
|
| 682 |
+
clip: rect(0,0,0,0);
|
| 683 |
+
border: 0;
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
.sp-import-btn {
|
| 687 |
+
display: inline-flex;
|
| 688 |
+
align-items: center;
|
| 689 |
+
gap: 4px;
|
| 690 |
+
padding: 3px 10px;
|
| 691 |
+
font-size: 11px;
|
| 692 |
+
font-family: var(--font-family-primary, 'Cairo', 'Tajawal', sans-serif);
|
| 693 |
+
color: var(--text-secondary);
|
| 694 |
+
border: 1px solid var(--color-border);
|
| 695 |
+
border-radius: var(--sp-radius-sm);
|
| 696 |
+
background: transparent;
|
| 697 |
+
cursor: pointer;
|
| 698 |
+
transition: all var(--sp-transition);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
.sp-import-btn:hover {
|
| 702 |
+
background: var(--color-surface-hover);
|
| 703 |
+
color: var(--text-color);
|
| 704 |
+
border-color: var(--primary-color);
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
/* ══════════════════════════════════════════════
|
| 708 |
+
Export dropdown
|
| 709 |
+
══════════════════════════════════════════════ */
|
| 710 |
+
.sp-export-dropdown {
|
| 711 |
+
position: relative;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
.sp-export-menu {
|
| 715 |
+
position: absolute;
|
| 716 |
+
top: 100%;
|
| 717 |
+
left: 0;
|
| 718 |
+
min-width: 130px;
|
| 719 |
+
background: var(--color-surface);
|
| 720 |
+
border: 1px solid var(--color-border);
|
| 721 |
+
border-radius: var(--sp-radius-sm);
|
| 722 |
+
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
|
| 723 |
+
z-index: 50;
|
| 724 |
+
overflow: hidden;
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
.sp-export-item {
|
| 728 |
+
display: block;
|
| 729 |
+
width: 100%;
|
| 730 |
+
padding: 8px 12px;
|
| 731 |
+
border: none;
|
| 732 |
+
background: transparent;
|
| 733 |
+
color: var(--text-color);
|
| 734 |
+
font-family: var(--font-family-primary, 'Cairo', 'Tajawal', sans-serif);
|
| 735 |
+
font-size: 12px;
|
| 736 |
+
text-align: right;
|
| 737 |
+
cursor: pointer;
|
| 738 |
+
transition: background var(--sp-transition);
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.sp-export-item:hover {
|
| 742 |
+
background: var(--color-surface-hover);
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
/* ══════════════════════════════════════════════
|
| 746 |
+
Length selector
|
| 747 |
══════════════════════════════════════════════ */
|
| 748 |
+
.sp-length-selector {
|
| 749 |
+
margin-bottom: 12px;
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
.sp-radio-group {
|
| 753 |
display: flex;
|
| 754 |
gap: 12px;
|
| 755 |
+
margin-top: 4px;
|
| 756 |
padding: 0 2px;
|
| 757 |
}
|
| 758 |
|
|
|
|
| 761 |
align-items: center;
|
| 762 |
gap: 4px;
|
| 763 |
font-size: 11px;
|
| 764 |
+
color: var(--text-secondary);
|
| 765 |
cursor: pointer;
|
| 766 |
}
|
| 767 |
.sp-radio input[type="radio"] {
|
| 768 |
+
accent-color: var(--primary-color);
|
| 769 |
width: 13px;
|
| 770 |
height: 13px;
|
| 771 |
}
|
| 772 |
|
| 773 |
+
/* ══════════════════════════════════════════════
|
| 774 |
+
Quran result (matching popup)
|
| 775 |
+
══════════════════════════════════════════════ */
|
| 776 |
+
.sp-quran-result {
|
| 777 |
+
background: var(--color-surface);
|
| 778 |
+
border: 1px solid rgba(6, 182, 212, 0.2);
|
| 779 |
+
border-radius: var(--sp-radius);
|
| 780 |
+
padding: 14px;
|
| 781 |
+
animation: spFadeIn 300ms ease;
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
.sp-quran-result-header {
|
| 785 |
+
display: flex;
|
| 786 |
+
align-items: center;
|
| 787 |
+
justify-content: space-between;
|
| 788 |
+
margin-bottom: 10px;
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
.sp-quran-uthmani {
|
| 792 |
+
font-family: 'Amiri Quran', 'Cairo', serif;
|
| 793 |
+
font-size: 20px;
|
| 794 |
+
line-height: 2.2;
|
| 795 |
+
text-align: center;
|
| 796 |
+
color: var(--text-color);
|
| 797 |
+
margin-bottom: 4px;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.sp-quran-reference {
|
| 801 |
+
text-align: center;
|
| 802 |
+
font-size: 12px;
|
| 803 |
+
font-weight: 600;
|
| 804 |
+
color: #06b6d4;
|
| 805 |
+
margin-bottom: 8px;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
.sp-quran-translate {
|
| 809 |
+
border-top: 1px solid var(--color-border);
|
| 810 |
+
padding-top: 10px;
|
| 811 |
+
margin-top: 10px;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
.sp-quran-translate-row {
|
| 815 |
+
display: flex;
|
| 816 |
+
align-items: center;
|
| 817 |
+
gap: 6px;
|
| 818 |
+
flex-wrap: wrap;
|
| 819 |
+
margin-bottom: 8px;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.sp-quran-translation {
|
| 823 |
+
padding: 10px;
|
| 824 |
+
border-radius: var(--sp-radius-sm);
|
| 825 |
+
background: rgba(6, 182, 212, 0.06);
|
| 826 |
+
border: 1px solid rgba(6, 182, 212, 0.15);
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
.sp-quran-translation p {
|
| 830 |
+
font-size: 16px;
|
| 831 |
+
line-height: 2;
|
| 832 |
+
text-align: center;
|
| 833 |
+
color: var(--text-color);
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
.sp-select {
|
| 837 |
+
flex: 1;
|
| 838 |
+
min-width: 140px;
|
| 839 |
+
padding: 6px 10px;
|
| 840 |
+
font-size: 13px;
|
| 841 |
+
color: var(--text-color);
|
| 842 |
+
background: var(--color-surface);
|
| 843 |
+
border: 1px solid var(--color-border);
|
| 844 |
+
border-radius: 8px;
|
| 845 |
+
cursor: pointer;
|
| 846 |
+
}
|
| 847 |
+
.sp-select:focus {
|
| 848 |
+
outline: none;
|
| 849 |
+
border-color: #06b6d4;
|
| 850 |
+
}
|
| 851 |
+
.sp-select option {
|
| 852 |
+
background: var(--color-surface);
|
| 853 |
+
color: var(--text-color);
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
/* ══════════════════════════════════════════════
|
| 857 |
Timing
|
| 858 |
══════════════════════════════════════════════ */
|
| 859 |
.sp-timing {
|
| 860 |
padding: 6px 10px;
|
| 861 |
margin-bottom: 8px;
|
| 862 |
+
background: var(--color-surface);
|
| 863 |
border-radius: var(--sp-radius-sm);
|
| 864 |
font-size: 10px;
|
| 865 |
color: var(--sp-text-muted);
|
|
|
|
| 878 |
align-items: center;
|
| 879 |
justify-content: center;
|
| 880 |
gap: 12px;
|
| 881 |
+
background: var(--color-overlay, rgba(15, 15, 20, 0.85));
|
| 882 |
backdrop-filter: blur(6px);
|
| 883 |
+
color: var(--text-color);
|
| 884 |
font-size: 13px;
|
| 885 |
}
|
| 886 |
|
| 887 |
.sp-spinner {
|
| 888 |
width: 32px;
|
| 889 |
height: 32px;
|
| 890 |
+
border: 3px solid var(--color-border);
|
| 891 |
+
border-top-color: var(--primary-color);
|
| 892 |
border-radius: 50%;
|
| 893 |
animation: spSpin 800ms linear infinite;
|
| 894 |
}
|
|
|
|
| 906 |
left: 50%;
|
| 907 |
transform: translateX(-50%) translateY(60px);
|
| 908 |
padding: 8px 16px;
|
| 909 |
+
background: var(--color-surface);
|
| 910 |
+
border: 1px solid var(--color-border);
|
| 911 |
border-radius: 20px;
|
| 912 |
+
color: var(--text-color);
|
| 913 |
font-size: 12px;
|
| 914 |
font-weight: 600;
|
| 915 |
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
|
|
| 934 |
filter: grayscale(30%);
|
| 935 |
transition: opacity var(--sp-transition), filter var(--sp-transition);
|
| 936 |
}
|
| 937 |
+
.sp-stale::after {
|
| 938 |
+
content: '⚠ أعد التحليل';
|
| 939 |
+
position: absolute;
|
| 940 |
+
top: 8px;
|
| 941 |
+
left: 50%;
|
| 942 |
+
transform: translateX(-50%);
|
| 943 |
+
padding: 4px 12px;
|
| 944 |
+
background: var(--sp-warning);
|
| 945 |
+
color: #000;
|
| 946 |
+
font-size: 10px;
|
| 947 |
+
font-weight: 700;
|
| 948 |
+
border-radius: 12px;
|
| 949 |
+
z-index: 10;
|
| 950 |
+
pointer-events: none;
|
| 951 |
+
white-space: nowrap;
|
| 952 |
+
}
|
| 953 |
|
| 954 |
/* ══════════════════════════════════════════════
|
| 955 |
Utilities
|
|
|
|
| 958 |
display: none !important;
|
| 959 |
}
|
| 960 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 961 |
/* Theme Toggle Button Styles */
|
| 962 |
.theme-toggle-animated {
|
| 963 |
display: flex;
|
|
|
|
| 967 |
height: 32px;
|
| 968 |
border: none;
|
| 969 |
border-radius: 50%;
|
| 970 |
+
background: var(--color-surface-hover);
|
| 971 |
+
color: var(--text-secondary);
|
| 972 |
cursor: pointer;
|
| 973 |
transition: background 0.3s ease, transform 0.3s ease, color 0.3s ease;
|
| 974 |
position: relative;
|
|
|
|
| 977 |
}
|
| 978 |
|
| 979 |
.theme-toggle-animated:hover {
|
| 980 |
+
background: var(--primary-color);
|
| 981 |
color: #fff;
|
| 982 |
transform: rotate(15deg);
|
| 983 |
}
|
|
|
|
| 1006 |
transform: rotate(0) scale(1);
|
| 1007 |
opacity: 1;
|
| 1008 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/sidepanel/sidepanel.html
CHANGED
|
@@ -3,7 +3,11 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
|
|
|
|
| 6 |
<title>بيان — لوحة التحليل</title>
|
|
|
|
| 7 |
<link rel="stylesheet" href="sidepanel.css">
|
| 8 |
</head>
|
| 9 |
<body>
|
|
@@ -11,12 +15,12 @@
|
|
| 11 |
<!-- Header -->
|
| 12 |
<!-- ══════════════════════════════════════════════ -->
|
| 13 |
<header class="sp-header">
|
| 14 |
-
<
|
| 15 |
<img src="../assets/icons/icon48.png" alt="بيان" width="24" height="24" style="border-radius:5px;">
|
| 16 |
-
<div style="height:24px; width:2px; background-color:#d1d5db; border-radius:9999px; flex-shrink:0;"></div>
|
| 17 |
<span class="sp-header-title">بيان</span>
|
| 18 |
-
</
|
| 19 |
-
|
| 20 |
<div style="display: flex; align-items: center; gap: 12px;">
|
| 21 |
<button id="ext-theme-toggle" class="theme-toggle-animated" aria-label="تبديل السمة" type="button">
|
| 22 |
<svg class="theme-icon-sun" width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
|
@@ -49,10 +53,6 @@
|
|
| 49 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
| 50 |
تدقيق النص القرآني
|
| 51 |
</button>
|
| 52 |
-
<button class="sp-tab" role="tab" aria-selected="false" data-tab="autocomplete" id="tab-autocomplete">
|
| 53 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
| 54 |
-
إكمال
|
| 55 |
-
</button>
|
| 56 |
</nav>
|
| 57 |
|
| 58 |
<!-- ══════════════════════════════════════════════ -->
|
|
@@ -61,6 +61,17 @@
|
|
| 61 |
<section class="sp-panel active" id="panel-correct" role="tabpanel">
|
| 62 |
<!-- Input area -->
|
| 63 |
<div class="sp-input-group">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
<textarea
|
| 65 |
id="input-text"
|
| 66 |
class="sp-textarea"
|
|
@@ -84,52 +95,41 @@
|
|
| 84 |
<button class="sp-btn sp-btn-ghost" id="btn-clear">مسح</button>
|
| 85 |
</div>
|
| 86 |
|
| 87 |
-
<!--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
<div class="sp-score is-hidden" id="score-section">
|
|
|
|
| 89 |
<div class="sp-score-ring">
|
| 90 |
-
<svg viewBox="0 0 160 160">
|
| 91 |
-
<
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
stroke-dasharray="440" stroke-dashoffset="440" stroke-linecap="round"
|
| 94 |
-
|
| 95 |
</svg>
|
| 96 |
<span class="sp-score-value" id="score-value">--</span>
|
| 97 |
</div>
|
| 98 |
-
<
|
| 99 |
-
|
| 100 |
-
<
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
<span class="sp-count-punctuation">ترقيم: <strong id="count-punctuation">٠</strong></span>
|
| 104 |
-
</div>
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
|
| 108 |
-
<!-- Corrected text result -->
|
| 109 |
-
<div class="sp-result is-hidden" id="result-section">
|
| 110 |
-
<div class="sp-result-header">
|
| 111 |
-
<span>النص المصحح</span>
|
| 112 |
-
<div class="sp-result-actions">
|
| 113 |
-
<button class="sp-btn-icon" id="btn-apply-page" title="تطبيق في الصفحة">
|
| 114 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 115 |
-
</button>
|
| 116 |
-
<button class="sp-btn-icon" id="btn-apply-all" title="تطبيق الكل">
|
| 117 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
| 118 |
-
</button>
|
| 119 |
-
<button class="sp-btn-icon" id="btn-copy-result" title="نسخ">
|
| 120 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
| 121 |
-
</button>
|
| 122 |
-
</div>
|
| 123 |
-
</div>
|
| 124 |
-
<div class="sp-result-text" id="result-text" dir="rtl"></div>
|
| 125 |
-
</div>
|
| 126 |
-
|
| 127 |
-
<!-- Suggestions list -->
|
| 128 |
-
<div class="sp-suggestions is-hidden" id="suggestions-section">
|
| 129 |
-
<div class="sp-suggestions-header">الاقتراحات</div>
|
| 130 |
-
<div class="sp-suggestions-list" id="suggestions-list" role="list"></div>
|
| 131 |
-
</div>
|
| 132 |
-
|
| 133 |
<!-- Timing info -->
|
| 134 |
<div class="sp-timing is-hidden" id="timing-section">
|
| 135 |
<span id="timing-text"></span>
|
|
@@ -140,7 +140,9 @@
|
|
| 140 |
<!-- Summarize Panel -->
|
| 141 |
<!-- ══════════════════════════════════════════════ -->
|
| 142 |
<section class="sp-panel" id="panel-summarize" role="tabpanel">
|
|
|
|
| 143 |
<div class="sp-input-group">
|
|
|
|
| 144 |
<textarea
|
| 145 |
id="summary-input-text"
|
| 146 |
class="sp-textarea"
|
|
@@ -149,18 +151,39 @@
|
|
| 149 |
rows="4"
|
| 150 |
maxlength="5000"
|
| 151 |
></textarea>
|
| 152 |
-
<div class="sp-meta-row">
|
| 153 |
-
<span class="sp-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
</div>
|
| 155 |
</div>
|
| 156 |
|
| 157 |
-
<!--
|
| 158 |
-
<div class="sp-
|
| 159 |
-
<
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
</div>
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
<div class="sp-actions">
|
| 165 |
<button class="sp-btn sp-btn-primary" id="btn-summarize">
|
| 166 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h10M4 18h14"/></svg>
|
|
@@ -170,14 +193,33 @@
|
|
| 170 |
|
| 171 |
<!-- Summary result -->
|
| 172 |
<div class="sp-result is-hidden" id="summary-result-section">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
<div class="sp-result-header">
|
| 174 |
-
<span>الملخص</span>
|
| 175 |
-
<
|
| 176 |
-
<
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
</div>
|
| 179 |
<div class="sp-result-text" id="summary-text" dir="rtl"></div>
|
| 180 |
-
<div class="sp-summary-meta" id="summary-meta"></div>
|
| 181 |
</div>
|
| 182 |
</section>
|
| 183 |
|
|
@@ -186,11 +228,13 @@
|
|
| 186 |
<!-- ══════════════════════════════════════════════ -->
|
| 187 |
<section class="sp-panel" id="panel-dialect" role="tabpanel">
|
| 188 |
<div class="sp-input-group">
|
|
|
|
|
|
|
| 189 |
<textarea
|
| 190 |
id="dialect-input-text"
|
| 191 |
class="sp-textarea"
|
| 192 |
dir="rtl"
|
| 193 |
-
placeholder="
|
| 194 |
rows="4"
|
| 195 |
maxlength="5000"
|
| 196 |
></textarea>
|
|
@@ -206,10 +250,15 @@
|
|
| 206 |
</div>
|
| 207 |
<div class="sp-result is-hidden" id="dialect-result-section">
|
| 208 |
<div class="sp-result-header">
|
| 209 |
-
<span>النص بالفصحى</span>
|
| 210 |
-
<
|
| 211 |
-
<
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
</div>
|
| 214 |
<div class="sp-result-text" id="dialect-text" dir="rtl"></div>
|
| 215 |
</div>
|
|
@@ -220,11 +269,12 @@
|
|
| 220 |
<!-- ══════════════════════════════════════════════ -->
|
| 221 |
<section class="sp-panel" id="panel-quran" role="tabpanel">
|
| 222 |
<div class="sp-input-group">
|
|
|
|
| 223 |
<textarea
|
| 224 |
id="quran-input-text"
|
| 225 |
class="sp-textarea"
|
| 226 |
dir="rtl"
|
| 227 |
-
placeholder="أدخل آية أو جزءاً منها
|
| 228 |
rows="4"
|
| 229 |
maxlength="2000"
|
| 230 |
></textarea>
|
|
@@ -235,24 +285,32 @@
|
|
| 235 |
<div class="sp-actions">
|
| 236 |
<button class="sp-btn sp-btn-primary" id="btn-quran">
|
| 237 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
| 238 |
-
تدقيق
|
| 239 |
</button>
|
| 240 |
</div>
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
</div>
|
| 248 |
-
<
|
| 249 |
-
<
|
| 250 |
|
| 251 |
-
<!--
|
| 252 |
-
<div class="sp-quran-translate
|
| 253 |
<div class="sp-quran-translate-row">
|
| 254 |
-
<
|
| 255 |
-
<
|
|
|
|
| 256 |
<option value="">— اختر لغة —</option>
|
| 257 |
<option value="english">🇬🇧 English</option>
|
| 258 |
<option value="french">🇫🇷 Français</option>
|
|
@@ -269,54 +327,22 @@
|
|
| 269 |
<option value="uzbek">🇺🇿 O'zbek</option>
|
| 270 |
</select>
|
| 271 |
</div>
|
| 272 |
-
<div
|
| 273 |
-
<
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
</div>
|
| 283 |
</div>
|
| 284 |
-
<div class="sp-result-text" id="quran-translation-text" dir="auto"></div>
|
| 285 |
-
<div class="sp-summary-meta" id="quran-translation-ref"></div>
|
| 286 |
</div>
|
| 287 |
</div>
|
| 288 |
</div>
|
| 289 |
</section>
|
| 290 |
|
| 291 |
-
<!-- ══════════════════════════════════════════════ -->
|
| 292 |
-
<!-- Autocomplete Panel (إكمال) -->
|
| 293 |
-
<!-- ══════════════════════════════════════════════ -->
|
| 294 |
-
<section class="sp-panel" id="panel-autocomplete" role="tabpanel">
|
| 295 |
-
<div class="sp-input-group">
|
| 296 |
-
<textarea
|
| 297 |
-
id="autocomplete-input-text"
|
| 298 |
-
class="sp-textarea"
|
| 299 |
-
dir="rtl"
|
| 300 |
-
placeholder="اكتب بداية الجملة واحصل على اقتراحات لإكمالها..."
|
| 301 |
-
rows="4"
|
| 302 |
-
maxlength="5000"
|
| 303 |
-
></textarea>
|
| 304 |
-
<div class="sp-meta-row">
|
| 305 |
-
<span class="sp-char-count"><span id="autocomplete-char-count">٠</span> حرف</span>
|
| 306 |
-
</div>
|
| 307 |
-
</div>
|
| 308 |
-
<div class="sp-actions">
|
| 309 |
-
<button class="sp-btn sp-btn-primary" id="btn-autocomplete">
|
| 310 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
| 311 |
-
اقترح إكمالاً
|
| 312 |
-
</button>
|
| 313 |
-
</div>
|
| 314 |
-
<div class="sp-suggestions is-hidden" id="autocomplete-result-section">
|
| 315 |
-
<div class="sp-suggestions-header">اقتراحات الإكمال</div>
|
| 316 |
-
<div class="sp-suggestions-list" id="autocomplete-list" role="list"></div>
|
| 317 |
-
</div>
|
| 318 |
-
</section>
|
| 319 |
-
|
| 320 |
<!-- ══════════════════════════════════════════════ -->
|
| 321 |
<!-- Loading Overlay -->
|
| 322 |
<!-- ═══════════════════════��══════════════════════ -->
|
|
@@ -329,8 +355,11 @@
|
|
| 329 |
<div class="sp-toast" id="toast"></div>
|
| 330 |
|
| 331 |
<!-- ══════════════════════════════════════════════ -->
|
| 332 |
-
<!-- Scripts —
|
| 333 |
<!-- ══════════════════════════════════════════════ -->
|
|
|
|
|
|
|
|
|
|
| 334 |
<script src="../shared/constants.js"></script>
|
| 335 |
<script src="../shared/config.js"></script>
|
| 336 |
<script src="../shared/bayan-renderer.js"></script>
|
|
@@ -339,4 +368,4 @@
|
|
| 339 |
<script src="../shared/bayan-api.js"></script>
|
| 340 |
<script src="sidepanel.js"></script>
|
| 341 |
</body>
|
| 342 |
-
</html>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 7 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
<title>بيان — لوحة التحليل</title>
|
| 10 |
+
<link rel="stylesheet" href="../shared/css/tokens.css">
|
| 11 |
<link rel="stylesheet" href="sidepanel.css">
|
| 12 |
</head>
|
| 13 |
<body>
|
|
|
|
| 15 |
<!-- Header -->
|
| 16 |
<!-- ══════════════════════════════════════════════ -->
|
| 17 |
<header class="sp-header">
|
| 18 |
+
<a href="https://bayan10-bayan-api.hf.space/" target="_blank" rel="noopener noreferrer" class="sp-header-brand" style="text-decoration:none;color:inherit;">
|
| 19 |
<img src="../assets/icons/icon48.png" alt="بيان" width="24" height="24" style="border-radius:5px;">
|
| 20 |
+
<div style="height:24px; width:2px; background-color:var(--color-border-strong, #d1d5db); border-radius:9999px; flex-shrink:0;"></div>
|
| 21 |
<span class="sp-header-title">بيان</span>
|
| 22 |
+
</a>
|
| 23 |
+
|
| 24 |
<div style="display: flex; align-items: center; gap: 12px;">
|
| 25 |
<button id="ext-theme-toggle" class="theme-toggle-animated" aria-label="تبديل السمة" type="button">
|
| 26 |
<svg class="theme-icon-sun" width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
|
|
|
| 53 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
| 54 |
تدقيق النص القرآني
|
| 55 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
</nav>
|
| 57 |
|
| 58 |
<!-- ══════════════════════════════════════════════ -->
|
|
|
|
| 61 |
<section class="sp-panel active" id="panel-correct" role="tabpanel">
|
| 62 |
<!-- Input area -->
|
| 63 |
<div class="sp-input-group">
|
| 64 |
+
<div class="sp-label-row">
|
| 65 |
+
<label class="sp-label" for="input-text">أدخل النص العربي</label>
|
| 66 |
+
<div style="display:flex; gap:4px;">
|
| 67 |
+
<button class="sp-btn-icon" id="btn-apply-page" type="button" title="تطبيق في الصفحة">
|
| 68 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 69 |
+
</button>
|
| 70 |
+
<button class="sp-btn-icon" id="btn-copy-text" type="button" title="نسخ النص">
|
| 71 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 72 |
+
</button>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
<textarea
|
| 76 |
id="input-text"
|
| 77 |
class="sp-textarea"
|
|
|
|
| 95 |
<button class="sp-btn sp-btn-ghost" id="btn-clear">مسح</button>
|
| 96 |
</div>
|
| 97 |
|
| 98 |
+
<!-- Suggestions list (above score — matching popup) -->
|
| 99 |
+
<div class="sp-suggestions is-hidden" id="suggestions-section">
|
| 100 |
+
<div class="sp-suggestions-header">
|
| 101 |
+
<span class="sp-suggestions-title">الاقتراحات</span>
|
| 102 |
+
<button class="sp-btn sp-btn-sm" id="btn-apply-all" type="button">تطبيق الكل</button>
|
| 103 |
+
</div>
|
| 104 |
+
<div class="sp-suggestions-list" id="suggestions-list" role="list"></div>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<!-- Score ring (matching popup — vertical centered with gradient) -->
|
| 108 |
<div class="sp-score is-hidden" id="score-section">
|
| 109 |
+
<span class="sp-score-title">تقييم الكتابة</span>
|
| 110 |
<div class="sp-score-ring">
|
| 111 |
+
<svg viewBox="0 0 160 160" width="120" height="120">
|
| 112 |
+
<defs>
|
| 113 |
+
<linearGradient id="scoreGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
| 114 |
+
<stop offset="0%" stop-color="var(--color-primary, #6BA3E0)"/>
|
| 115 |
+
<stop offset="100%" stop-color="var(--color-secondary, #A594E8)"/>
|
| 116 |
+
</linearGradient>
|
| 117 |
+
</defs>
|
| 118 |
+
<circle cx="80" cy="80" r="70" fill="none" stroke="var(--color-border)" stroke-width="10"/>
|
| 119 |
+
<circle cx="80" cy="80" r="70" fill="none" stroke="url(#scoreGradient)" stroke-width="10"
|
| 120 |
stroke-dasharray="440" stroke-dashoffset="440" stroke-linecap="round"
|
| 121 |
+
transform="rotate(-90 80 80)" id="score-circle"/>
|
| 122 |
</svg>
|
| 123 |
<span class="sp-score-value" id="score-value">--</span>
|
| 124 |
</div>
|
| 125 |
+
<span class="sp-score-hint" id="score-hint">ابدأ الكتابة لرؤية تقييمك</span>
|
| 126 |
+
<div class="sp-score-counts">
|
| 127 |
+
<span class="sp-count sp-count-spelling"><span id="count-spelling">٠</span> إملائي</span>
|
| 128 |
+
<span class="sp-count sp-count-grammar"><span id="count-grammar">٠</span> نحوي</span>
|
| 129 |
+
<span class="sp-count sp-count-punctuation"><span id="count-punctuation">٠</span> ترقيم</span>
|
|
|
|
|
|
|
| 130 |
</div>
|
| 131 |
</div>
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
<!-- Timing info -->
|
| 134 |
<div class="sp-timing is-hidden" id="timing-section">
|
| 135 |
<span id="timing-text"></span>
|
|
|
|
| 140 |
<!-- Summarize Panel -->
|
| 141 |
<!-- ══════════════════════════════════════════════ -->
|
| 142 |
<section class="sp-panel" id="panel-summarize" role="tabpanel">
|
| 143 |
+
<!-- Input area -->
|
| 144 |
<div class="sp-input-group">
|
| 145 |
+
<label class="sp-label" for="summary-input-text">أدخل النص للتلخيص</label>
|
| 146 |
<textarea
|
| 147 |
id="summary-input-text"
|
| 148 |
class="sp-textarea"
|
|
|
|
| 151 |
rows="4"
|
| 152 |
maxlength="5000"
|
| 153 |
></textarea>
|
| 154 |
+
<div class="sp-input-meta-row">
|
| 155 |
+
<span class="sp-input-meta"><span id="summary-word-count-input">٠</span> كلمة</span>
|
| 156 |
+
<label class="sp-import-btn" title="استيراد ملف">
|
| 157 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
|
| 158 |
+
استيراد ملف
|
| 159 |
+
<input type="file" id="summary-import-input" class="sp-sr-only" accept=".txt,.docx">
|
| 160 |
+
</label>
|
| 161 |
</div>
|
| 162 |
</div>
|
| 163 |
|
| 164 |
+
<!-- Mode toggle (paragraph / bullets) -->
|
| 165 |
+
<div class="sp-mode-toggle">
|
| 166 |
+
<button type="button" class="sp-mode-btn active" id="summary-mode-paragraph" data-mode="paragraph">
|
| 167 |
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 24 24"><path d="M3 5h18v2H3V5zm0 8h18v2H3v-2zm0 4h12v2H3v-2z"/></svg>
|
| 168 |
+
فقرة
|
| 169 |
+
</button>
|
| 170 |
+
<button type="button" class="sp-mode-btn" id="summary-mode-bullets" data-mode="bullets">
|
| 171 |
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 24 24"><path d="M4 6h2v2H4V6zm4 0h12v2H8V6zM4 11h2v2H4v-2zm4 0h12v2H8v-2zm-4 5h2v2H4v-2zm4 0h12v2H8v-2z"/></svg>
|
| 172 |
+
نقاط
|
| 173 |
+
</button>
|
| 174 |
</div>
|
| 175 |
|
| 176 |
+
<!-- Length selector -->
|
| 177 |
+
<div class="sp-length-selector">
|
| 178 |
+
<label class="sp-label">طول الملخص</label>
|
| 179 |
+
<div class="sp-radio-group">
|
| 180 |
+
<label class="sp-radio"><input type="radio" name="summary-length" value="1"> قصير</label>
|
| 181 |
+
<label class="sp-radio"><input type="radio" name="summary-length" value="2" checked> متوسط</label>
|
| 182 |
+
<label class="sp-radio"><input type="radio" name="summary-length" value="3"> طويل</label>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<!-- Action -->
|
| 187 |
<div class="sp-actions">
|
| 188 |
<button class="sp-btn sp-btn-primary" id="btn-summarize">
|
| 189 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h10M4 18h14"/></svg>
|
|
|
|
| 193 |
|
| 194 |
<!-- Summary result -->
|
| 195 |
<div class="sp-result is-hidden" id="summary-result-section">
|
| 196 |
+
<!-- Summary stats -->
|
| 197 |
+
<div class="sp-summary-stats is-hidden" id="summary-stats">
|
| 198 |
+
<div class="sp-summary-stat"><span>كلمات الملخص:</span> <span class="sp-summary-stat-value" id="summary-word-count">٠</span></div>
|
| 199 |
+
<div class="sp-summary-stat"><span>نسبة الاختصار:</span> <span class="sp-summary-stat-value" id="summary-compression">٠٪</span></div>
|
| 200 |
+
</div>
|
| 201 |
<div class="sp-result-header">
|
| 202 |
+
<span class="sp-result-title">الملخص</span>
|
| 203 |
+
<div class="sp-result-actions">
|
| 204 |
+
<button class="sp-btn-icon" id="btn-apply-summary" type="button" title="تطبيق في الصفحة">
|
| 205 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 206 |
+
</button>
|
| 207 |
+
<button class="sp-btn-icon" id="btn-copy-summary" type="button" title="نسخ">
|
| 208 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 209 |
+
</button>
|
| 210 |
+
<div class="sp-export-dropdown" id="summary-export-wrap">
|
| 211 |
+
<button class="sp-btn-icon" id="btn-export-summary" type="button" title="تصدير">
|
| 212 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M12 4v12m0 0l-4-4m4 4l4-4"/></svg>
|
| 213 |
+
</button>
|
| 214 |
+
<div class="sp-export-menu is-hidden" id="summary-export-menu">
|
| 215 |
+
<button type="button" class="sp-export-item" data-format="txt">نصي (.txt)</button>
|
| 216 |
+
<button type="button" class="sp-export-item" data-format="docx">Word (.docx)</button>
|
| 217 |
+
<button type="button" class="sp-export-item" data-format="pdf">PDF (.pdf)</button>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
</div>
|
| 222 |
<div class="sp-result-text" id="summary-text" dir="rtl"></div>
|
|
|
|
| 223 |
</div>
|
| 224 |
</section>
|
| 225 |
|
|
|
|
| 228 |
<!-- ══════════════════════════════════════════════ -->
|
| 229 |
<section class="sp-panel" id="panel-dialect" role="tabpanel">
|
| 230 |
<div class="sp-input-group">
|
| 231 |
+
<label class="sp-label" for="dialect-input-text">تحويل اللهجات للفصحى</label>
|
| 232 |
+
<p class="sp-description">اكتب نصًا بأي لهجة عربية (مصرية، خليجية، شامية...) وسنحوّله إلى لغة عربية فصحى سليمة.</p>
|
| 233 |
<textarea
|
| 234 |
id="dialect-input-text"
|
| 235 |
class="sp-textarea"
|
| 236 |
dir="rtl"
|
| 237 |
+
placeholder="اكتب النص باللهجة هنا..."
|
| 238 |
rows="4"
|
| 239 |
maxlength="5000"
|
| 240 |
></textarea>
|
|
|
|
| 250 |
</div>
|
| 251 |
<div class="sp-result is-hidden" id="dialect-result-section">
|
| 252 |
<div class="sp-result-header">
|
| 253 |
+
<span class="sp-result-title" style="color: var(--color-primary, #6BA3E0);">✓ النص بالعربية الفصحى</span>
|
| 254 |
+
<div class="sp-result-actions">
|
| 255 |
+
<button class="sp-btn-icon" id="btn-apply-dialect" type="button" title="تطبيق في الصفحة">
|
| 256 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 257 |
+
</button>
|
| 258 |
+
<button class="sp-btn-icon" id="btn-copy-dialect" type="button" title="نسخ">
|
| 259 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 260 |
+
</button>
|
| 261 |
+
</div>
|
| 262 |
</div>
|
| 263 |
<div class="sp-result-text" id="dialect-text" dir="rtl"></div>
|
| 264 |
</div>
|
|
|
|
| 269 |
<!-- ══════════════════════════════════════════════ -->
|
| 270 |
<section class="sp-panel" id="panel-quran" role="tabpanel">
|
| 271 |
<div class="sp-input-group">
|
| 272 |
+
<label class="sp-label" for="quran-input-text">تدقيق الآيات القرآنية</label>
|
| 273 |
<textarea
|
| 274 |
id="quran-input-text"
|
| 275 |
class="sp-textarea"
|
| 276 |
dir="rtl"
|
| 277 |
+
placeholder="أدخل آية أو جزءاً منها..."
|
| 278 |
rows="4"
|
| 279 |
maxlength="2000"
|
| 280 |
></textarea>
|
|
|
|
| 285 |
<div class="sp-actions">
|
| 286 |
<button class="sp-btn sp-btn-primary" id="btn-quran">
|
| 287 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
| 288 |
+
بحث وتدقيق
|
| 289 |
</button>
|
| 290 |
</div>
|
| 291 |
+
|
| 292 |
+
<!-- Quran result (matching popup structure) -->
|
| 293 |
+
<div class="sp-quran-result is-hidden" id="quran-result-section">
|
| 294 |
+
<div class="sp-quran-result-header">
|
| 295 |
+
<span style="color:#06b6d4; font-size:12px; font-weight:700;">✓ النص القرآني المدقق</span>
|
| 296 |
+
<div class="sp-result-actions">
|
| 297 |
+
<button class="sp-btn-icon" id="btn-apply-quran" type="button" title="تطبيق في الصفحة">
|
| 298 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 299 |
+
</button>
|
| 300 |
+
<button class="sp-btn-icon" id="btn-copy-quran" type="button" title="نسخ">
|
| 301 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 302 |
+
</button>
|
| 303 |
+
</div>
|
| 304 |
</div>
|
| 305 |
+
<p id="quran-uthmani" class="sp-quran-uthmani" dir="rtl"></p>
|
| 306 |
+
<p id="quran-reference" class="sp-quran-reference"></p>
|
| 307 |
|
| 308 |
+
<!-- Translation -->
|
| 309 |
+
<div class="sp-quran-translate">
|
| 310 |
<div class="sp-quran-translate-row">
|
| 311 |
+
<svg width="14" height="14" fill="none" stroke="#06b6d4" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"/></svg>
|
| 312 |
+
<span style="font-size:12px; font-weight:600; color:var(--text-secondary);">ترجمة الآية</span>
|
| 313 |
+
<select id="quran-lang-select" class="sp-select">
|
| 314 |
<option value="">— اختر لغة —</option>
|
| 315 |
<option value="english">🇬🇧 English</option>
|
| 316 |
<option value="french">🇫🇷 Français</option>
|
|
|
|
| 327 |
<option value="uzbek">🇺🇿 O'zbek</option>
|
| 328 |
</select>
|
| 329 |
</div>
|
| 330 |
+
<div id="quran-translation-section" class="sp-quran-translation is-hidden">
|
| 331 |
+
<p id="quran-trans-text" dir="auto"></p>
|
| 332 |
+
<p id="quran-trans-ref" class="sp-quran-reference" style="display:none;"></p>
|
| 333 |
+
<div class="sp-result-actions" id="quran-trans-actions" style="display:none; justify-content:flex-end; gap:4px; margin-top:8px;">
|
| 334 |
+
<button class="sp-btn-icon" id="btn-apply-quran-trans" type="button" title="تطبيق الترجمة في الصفحة">
|
| 335 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 336 |
+
</button>
|
| 337 |
+
<button class="sp-btn-icon" id="btn-copy-quran-trans" type="button" title="نسخ الترجمة">
|
| 338 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 339 |
+
</button>
|
|
|
|
| 340 |
</div>
|
|
|
|
|
|
|
| 341 |
</div>
|
| 342 |
</div>
|
| 343 |
</div>
|
| 344 |
</section>
|
| 345 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
<!-- ══════════════════════════════════════════════ -->
|
| 347 |
<!-- Loading Overlay -->
|
| 348 |
<!-- ═══════════════════════��══════════════════════ -->
|
|
|
|
| 355 |
<div class="sp-toast" id="toast"></div>
|
| 356 |
|
| 357 |
<!-- ══════════════════════════════════════════════ -->
|
| 358 |
+
<!-- Scripts — vendor libs + shared modules -->
|
| 359 |
<!-- ══════════════════════════════════════════════ -->
|
| 360 |
+
<script src="../shared/vendor/mammoth.browser.min.js"></script>
|
| 361 |
+
<script src="../shared/vendor/docx.umd.js"></script>
|
| 362 |
+
<script src="../shared/vendor/html2pdf.bundle.min.js"></script>
|
| 363 |
<script src="../shared/constants.js"></script>
|
| 364 |
<script src="../shared/config.js"></script>
|
| 365 |
<script src="../shared/bayan-renderer.js"></script>
|
|
|
|
| 368 |
<script src="../shared/bayan-api.js"></script>
|
| 369 |
<script src="sidepanel.js"></script>
|
| 370 |
</body>
|
| 371 |
+
</html>
|
extension/sidepanel/sidepanel.js
CHANGED
|
@@ -1,18 +1,13 @@
|
|
| 1 |
/**
|
| 2 |
* Bayan Chrome Extension — Side Panel Logic
|
| 3 |
*
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
* Reuses:
|
| 7 |
-
* - bayanAnalyze(), bayanSummarize(), bayanHealthCheck() from bayan-api.js
|
| 8 |
-
* - renderHighlightedText(), escapeHtml() from bayan-renderer.js
|
| 9 |
-
* - buildSuggestionCardHTML(), calculateWritingScore(), getScoreHint() from bayan-ui.js
|
| 10 |
-
* - applyAndRebase(), applyAllPatches(), sortSuggestions(), countByType(), removeSuggestion() from bayan-patches.js
|
| 11 |
*
|
| 12 |
* Key differences from popup.js:
|
| 13 |
* - Persistent: panel stays open across page navigations
|
| 14 |
* - Auto-analysis: text injected from context menu auto-analyzes
|
| 15 |
* - Debounced live updates: re-analyzes on user edits (500ms debounce)
|
|
|
|
| 16 |
* - State persistence: last analysis saved to chrome.storage.session
|
| 17 |
*/
|
| 18 |
|
|
@@ -28,10 +23,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 28 |
const btnClear = document.getElementById('btn-clear');
|
| 29 |
const btnApplyAll = document.getElementById('btn-apply-all');
|
| 30 |
const btnApplyPage = document.getElementById('btn-apply-page');
|
| 31 |
-
const
|
| 32 |
const scoreSection = document.getElementById('score-section');
|
| 33 |
-
const resultSection = document.getElementById('result-section');
|
| 34 |
-
const resultText = document.getElementById('result-text');
|
| 35 |
const suggestionsSection = document.getElementById('suggestions-section');
|
| 36 |
const suggestionsList = document.getElementById('suggestions-list');
|
| 37 |
const timingSection = document.getElementById('timing-section');
|
|
@@ -41,11 +34,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 41 |
|
| 42 |
// Summary tab
|
| 43 |
const summaryInputText = document.getElementById('summary-input-text');
|
| 44 |
-
const
|
| 45 |
const btnSummarize = document.getElementById('btn-summarize');
|
| 46 |
const summaryResultSection = document.getElementById('summary-result-section');
|
| 47 |
const summaryText = document.getElementById('summary-text');
|
| 48 |
-
const
|
|
|
|
|
|
|
| 49 |
const btnCopySummary = document.getElementById('btn-copy-summary');
|
| 50 |
|
| 51 |
// Score
|
|
@@ -64,14 +59,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 64 |
let isStale = false;
|
| 65 |
let isAnalyzing = false;
|
| 66 |
let contextConsumed = false;
|
| 67 |
-
let debounceTimer = null;
|
| 68 |
-
// The exact text the user had selected on the page when this action started
|
| 69 |
-
// (from the right-click context menu). Used as a precise find/replace anchor
|
| 70 |
-
// so write-back replaces ONLY that selection, never the whole field.
|
| 71 |
let sourceSelectionText = '';
|
|
|
|
| 72 |
|
| 73 |
const SCORE_CIRCUMFERENCE = 440;
|
| 74 |
-
const DEBOUNCE_MS = 500;
|
| 75 |
|
| 76 |
// ══════════════════════════════════════════════════════════
|
| 77 |
// Tab switching
|
|
@@ -86,6 +77,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 86 |
document.querySelectorAll('.sp-panel').forEach((p) => {
|
| 87 |
p.classList.toggle('active', p.id === `panel-${targetTab}`);
|
| 88 |
});
|
|
|
|
| 89 |
});
|
| 90 |
});
|
| 91 |
|
|
@@ -102,24 +94,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 102 |
|
| 103 |
inputText.addEventListener('input', () => {
|
| 104 |
updateCounts(inputText, charCount, wordCount);
|
|
|
|
| 105 |
|
| 106 |
-
// Staleness detection
|
| 107 |
if (currentSuggestions.length > 0 && inputText.value !== analyzedText) {
|
| 108 |
markStale();
|
| 109 |
}
|
| 110 |
-
|
| 111 |
-
// Debounced live re-analysis
|
| 112 |
-
if (debounceTimer) clearTimeout(debounceTimer);
|
| 113 |
-
debounceTimer = setTimeout(() => {
|
| 114 |
-
const text = inputText.value.trim();
|
| 115 |
-
if (text.length >= CONFIG.MIN_ANALYZE_LENGTH && !isAnalyzing) {
|
| 116 |
-
runAnalysis(text);
|
| 117 |
-
}
|
| 118 |
-
}, DEBOUNCE_MS);
|
| 119 |
});
|
| 120 |
|
| 121 |
summaryInputText.addEventListener('input', () => {
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
| 123 |
});
|
| 124 |
|
| 125 |
// ══════════════════════════════════════════════════════════
|
|
@@ -128,14 +114,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 128 |
function markStale() {
|
| 129 |
if (isStale) return;
|
| 130 |
isStale = true;
|
| 131 |
-
if (resultSection) resultSection.classList.add('sp-stale');
|
| 132 |
if (suggestionsSection) suggestionsSection.classList.add('sp-stale');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
|
| 135 |
function clearStale() {
|
| 136 |
isStale = false;
|
| 137 |
-
if (resultSection) resultSection.classList.remove('sp-stale');
|
| 138 |
if (suggestionsSection) suggestionsSection.classList.remove('sp-stale');
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
| 140 |
|
| 141 |
// ══════════════════════════════════════════════════════════
|
|
@@ -156,13 +147,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 156 |
|
| 157 |
// ══════════════════════════════════════════════════════════
|
| 158 |
// Write-back to the page field (panel → background → content script)
|
| 159 |
-
// The side panel is a separate document and cannot touch page DOM
|
| 160 |
-
// directly; it relays through background.js. `source` lets the content
|
| 161 |
-
// script decide whether to re-analyze (correct) or suppress (Change 3).
|
| 162 |
-
//
|
| 163 |
-
// `find` (optional) is the exact original selected text. When present, the
|
| 164 |
-
// content script replaces ONLY that occurrence in the field — the most
|
| 165 |
-
// reliable way to scope the replacement to the user's selection.
|
| 166 |
// ══════════════════════════════════════════════════════════
|
| 167 |
function writeBackToPage(text, mode = 'auto', source = 'correct', find = '') {
|
| 168 |
try {
|
|
@@ -208,14 +192,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 208 |
|
| 209 |
if (!suggestions || suggestions.length === 0) {
|
| 210 |
suggestionsSection.classList.add('is-hidden');
|
| 211 |
-
btnApplyAll.classList.add('is-hidden');
|
| 212 |
return;
|
| 213 |
}
|
| 214 |
|
| 215 |
suggestionsSection.classList.remove('is-hidden');
|
| 216 |
suggestionsList.innerHTML = suggestions.map((s, i) => buildSuggestionCardHTML(s, i)).join('');
|
| 217 |
|
| 218 |
-
// Bind alt-chip click events
|
| 219 |
suggestionsList.querySelectorAll('.bayan-alt-chip').forEach((chip) => {
|
| 220 |
chip.addEventListener('click', (e) => {
|
| 221 |
e.stopPropagation();
|
|
@@ -243,7 +225,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 243 |
const counts = countByType(currentSuggestions);
|
| 244 |
updateScore(counts.spelling, counts.grammar, counts.punctuation);
|
| 245 |
renderSuggestions(currentSuggestions);
|
| 246 |
-
resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
|
| 247 |
saveState();
|
| 248 |
showToast('✓ تم التصحيح');
|
| 249 |
});
|
|
@@ -256,8 +237,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 256 |
// Core analysis function
|
| 257 |
// ══════════════════════════════════════════════════════════
|
| 258 |
async function runAnalysis(text) {
|
| 259 |
-
if (!text || text.length <
|
| 260 |
-
showToast('
|
| 261 |
return;
|
| 262 |
}
|
| 263 |
if (text.length > CONFIG.MAX_ANALYZE_LENGTH) {
|
|
@@ -280,9 +261,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 280 |
inputText.value = analyzedText;
|
| 281 |
updateCounts(inputText, charCount, wordCount);
|
| 282 |
|
| 283 |
-
resultSection.classList.remove('is-hidden');
|
| 284 |
-
resultText.innerHTML = renderHighlightedText(analyzedText, suggestions);
|
| 285 |
-
|
| 286 |
const counts = countByType(suggestions);
|
| 287 |
updateScore(counts.spelling, counts.grammar, counts.punctuation);
|
| 288 |
renderSuggestions(suggestions);
|
|
@@ -319,10 +297,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 319 |
function saveState() {
|
| 320 |
const storage = getStorage();
|
| 321 |
if (!storage) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
storage.set({
|
| 323 |
spLastText: analyzedText,
|
| 324 |
spLastSuggestions: currentSuggestions,
|
| 325 |
spLastInput: inputText.value,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
}).catch(() => {});
|
| 327 |
}
|
| 328 |
|
|
@@ -331,7 +317,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 331 |
if (!storage) return false;
|
| 332 |
|
| 333 |
try {
|
| 334 |
-
const data = await storage.get(['spLastText', 'spLastSuggestions', 'spLastInput']);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
if (!data.spLastText || !data.spLastSuggestions) return false;
|
| 336 |
|
| 337 |
analyzedText = data.spLastText;
|
|
@@ -339,9 +348,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 339 |
inputText.value = data.spLastInput || analyzedText;
|
| 340 |
updateCounts(inputText, charCount, wordCount);
|
| 341 |
|
| 342 |
-
resultSection.classList.remove('is-hidden');
|
| 343 |
-
resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
|
| 344 |
-
|
| 345 |
const counts = countByType(currentSuggestions);
|
| 346 |
updateScore(counts.spelling, counts.grammar, counts.punctuation);
|
| 347 |
renderSuggestions(currentSuggestions);
|
|
@@ -366,7 +372,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 366 |
sourceSelectionText = '';
|
| 367 |
updateCounts(inputText, charCount, wordCount);
|
| 368 |
scoreSection.classList.add('is-hidden');
|
| 369 |
-
resultSection.classList.add('is-hidden');
|
| 370 |
suggestionsSection.classList.add('is-hidden');
|
| 371 |
timingSection.classList.add('is-hidden');
|
| 372 |
currentSuggestions = [];
|
|
@@ -386,7 +391,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 386 |
inputText.value = analyzedText;
|
| 387 |
updateCounts(inputText, charCount, wordCount);
|
| 388 |
currentSuggestions = [];
|
| 389 |
-
resultText.innerHTML = escapeHtml(analyzedText);
|
| 390 |
updateScore(0, 0, 0);
|
| 391 |
renderSuggestions([]);
|
| 392 |
saveState();
|
|
@@ -394,25 +398,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 394 |
showToast('✓ تم تطبيق جميع التصحيحات');
|
| 395 |
});
|
| 396 |
|
| 397 |
-
// Explicit "apply corrected text to the page field" button (Bug 1).
|
| 398 |
-
// Writes the current corrected text — with any still-pending suggestions
|
| 399 |
-
// applied — back into the source field, honouring selection vs whole-field.
|
| 400 |
if (btnApplyPage) {
|
| 401 |
btnApplyPage.addEventListener('click', () => {
|
| 402 |
-
if (!analyzedText) { showToast('لا يوجد نص للتطبيق'); return; }
|
| 403 |
if (isStale) { showToast('⚠ أعد التحليل أولاً — النص تغيّر'); return; }
|
| 404 |
const finalText = currentSuggestions.length > 0
|
| 405 |
? applyAllPatches(analyzedText, currentSuggestions)
|
| 406 |
-
: analyzedText;
|
| 407 |
writeBackToPage(finalText, 'auto', 'correct', sourceSelectionText);
|
| 408 |
});
|
| 409 |
}
|
| 410 |
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
.
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
});
|
| 417 |
|
| 418 |
// ════════════════════════════════���═════════════════════════
|
|
@@ -430,8 +445,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 430 |
const data = await bayanSummarize(text, lengthValue);
|
| 431 |
if (data.status === 'success' && data.summary) {
|
| 432 |
summaryResultSection.classList.remove('is-hidden');
|
| 433 |
-
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
showToast('✓ تم التلخيص');
|
| 436 |
} else {
|
| 437 |
showToast('تعذّر التلخيص — حاول مرة أخرى');
|
|
@@ -445,12 +487,149 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 445 |
});
|
| 446 |
|
| 447 |
btnCopySummary.addEventListener('click', () => {
|
| 448 |
-
const text = summaryText.textContent || '';
|
| 449 |
navigator.clipboard.writeText(text)
|
| 450 |
.then(() => showToast('✓ تم نسخ الملخص'))
|
| 451 |
.catch(() => showToast('تعذّر النسخ'));
|
| 452 |
});
|
| 453 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
// ══════════════════════════════════════════════════════════
|
| 455 |
// Dialect → MSA conversion
|
| 456 |
// ══════════════════════════════════════════════════════════
|
|
@@ -462,7 +641,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 462 |
const btnCopyDialect = document.getElementById('btn-copy-dialect');
|
| 463 |
|
| 464 |
if (dialectInput) {
|
| 465 |
-
dialectInput.addEventListener('input', () =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
|
| 467 |
btnDialect.addEventListener('click', async () => {
|
| 468 |
const text = dialectInput.value.trim();
|
|
@@ -491,241 +674,155 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 491 |
.then(() => showToast('✓ تم نسخ النص'))
|
| 492 |
.catch(() => showToast('تعذّر النسخ'));
|
| 493 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
}
|
| 495 |
|
| 496 |
// ══════════════════════════════════════════════════════════
|
| 497 |
-
// Quran verification
|
| 498 |
// ══════════════════════════════════════════════════════════
|
| 499 |
const quranInput = document.getElementById('quran-input-text');
|
| 500 |
const quranCharCount = document.getElementById('quran-char-count');
|
| 501 |
const btnQuran = document.getElementById('btn-quran');
|
| 502 |
const quranResultSection = document.getElementById('quran-result-section');
|
| 503 |
-
const
|
| 504 |
-
const
|
| 505 |
const btnCopyQuran = document.getElementById('btn-copy-quran');
|
| 506 |
-
// Translation sub-UI
|
| 507 |
-
const quranTranslateSection = document.getElementById('quran-translate-section');
|
| 508 |
const quranLangSelect = document.getElementById('quran-lang-select');
|
| 509 |
-
const
|
| 510 |
-
const
|
| 511 |
-
const
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
let
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
seg = seg || '';
|
| 519 |
-
const refMatch = seg.match(/【([^】]+)】/);
|
| 520 |
-
const text = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 521 |
-
return { text, ref: refMatch ? refMatch[1] : '' };
|
| 522 |
-
}
|
| 523 |
|
| 524 |
if (quranInput) {
|
| 525 |
-
quranInput.addEventListener('input', () => updateCounts(quranInput, quranCharCount, null));
|
| 526 |
|
| 527 |
btnQuran.addEventListener('click', async () => {
|
| 528 |
const text = quranInput.value.trim();
|
| 529 |
if (!text) { showToast('أدخل آية للتدقيق'); return; }
|
| 530 |
|
|
|
|
| 531 |
setLoading(true, 'جارٍ التدقيق...');
|
|
|
|
|
|
|
|
|
|
| 532 |
try {
|
| 533 |
const data = await bayanQuran(text);
|
| 534 |
quranResultSection.classList.remove('is-hidden');
|
| 535 |
-
|
| 536 |
-
if (quranTranslateSection) quranTranslateSection.classList.add('is-hidden');
|
| 537 |
-
if (quranTranslationResult) quranTranslationResult.classList.add('is-hidden');
|
| 538 |
-
if (quranLangSelect) quranLangSelect.value = '';
|
| 539 |
if (data.error) {
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
quranText.textContent = data.full_verse || data.matched_segment || JSON.stringify(data);
|
| 544 |
-
quranMeta.textContent = data.matched_segment && data.full_verse
|
| 545 |
-
? `المقطع المطابق: ${data.matched_segment}`
|
| 546 |
-
: '';
|
| 547 |
-
// Enable translation now that we have a verified verse.
|
| 548 |
-
lastQuranQuery = text;
|
| 549 |
-
if (quranTranslateSection) quranTranslateSection.classList.remove('is-hidden');
|
| 550 |
-
showToast('✓ تم التدقيق');
|
| 551 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
} catch (error) {
|
| 553 |
console.error('[Bayan SP] Quran error:', error);
|
| 554 |
-
|
|
|
|
|
|
|
| 555 |
} finally {
|
| 556 |
setLoading(false);
|
| 557 |
}
|
| 558 |
});
|
| 559 |
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
const lang = quranLangSelect.value;
|
| 564 |
-
if (!lang || !lastQuranQuery) return;
|
| 565 |
-
|
| 566 |
-
setLoading(true, 'جارٍ الترجمة...');
|
| 567 |
-
try {
|
| 568 |
-
const data = await bayanQuran(lastQuranQuery, lang);
|
| 569 |
-
quranTranslationResult.classList.remove('is-hidden');
|
| 570 |
-
if (data.error) {
|
| 571 |
-
quranTranslationText.textContent = data.error;
|
| 572 |
-
quranTranslationRef.textContent = '';
|
| 573 |
-
} else {
|
| 574 |
-
const parsed = parseQuranSegment(data.matched_segment || data.full_verse || '');
|
| 575 |
-
quranTranslationText.textContent = parsed.text || data.full_verse || '';
|
| 576 |
-
quranTranslationRef.textContent = parsed.ref ? `[${parsed.ref}]` : '';
|
| 577 |
-
showToast('✓ تمت الترجمة');
|
| 578 |
-
}
|
| 579 |
-
} catch (error) {
|
| 580 |
-
console.error('[Bayan SP] Quran translation error:', error);
|
| 581 |
-
showToast('خطأ في الاتصال — تحقق من الإنترنت');
|
| 582 |
-
} finally {
|
| 583 |
-
setLoading(false);
|
| 584 |
-
}
|
| 585 |
-
});
|
| 586 |
-
}
|
| 587 |
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
.then(() => showToast('✓ تم نسخ الترجمة'))
|
| 592 |
-
.catch(() => showToast('تعذّر النسخ'));
|
| 593 |
-
});
|
| 594 |
-
}
|
| 595 |
|
| 596 |
-
// Apply the translated verse straight into the page field (Req 2).
|
| 597 |
-
// Routed as 'quran' so the content script suppresses correction re-analysis.
|
| 598 |
-
if (btnApplyQuranTranslation) {
|
| 599 |
-
btnApplyQuranTranslation.addEventListener('click', () => {
|
| 600 |
-
const text = (quranTranslationText.textContent || '').trim();
|
| 601 |
-
if (!text) { showToast('لا توجد ترجمة للتطبيق'); return; }
|
| 602 |
-
writeBackToPage(text, 'auto', 'quran', sourceSelectionText);
|
| 603 |
-
});
|
| 604 |
-
}
|
| 605 |
-
|
| 606 |
-
btnCopyQuran.addEventListener('click', () => {
|
| 607 |
-
navigator.clipboard.writeText(quranText.textContent || '')
|
| 608 |
-
.then(() => showToast('✓ تم النسخ'))
|
| 609 |
-
.catch(() => showToast('تعذّر النسخ'));
|
| 610 |
-
});
|
| 611 |
-
}
|
| 612 |
-
|
| 613 |
-
// ══════════════════════════════════════════════════════════
|
| 614 |
-
// Autocomplete suggestions
|
| 615 |
-
// ══════════════════════════════════════════════════════════
|
| 616 |
-
const acInput = document.getElementById('autocomplete-input-text');
|
| 617 |
-
const acCharCount = document.getElementById('autocomplete-char-count');
|
| 618 |
-
const btnAutocomplete = document.getElementById('btn-autocomplete');
|
| 619 |
-
const acResultSection = document.getElementById('autocomplete-result-section');
|
| 620 |
-
const acList = document.getElementById('autocomplete-list');
|
| 621 |
-
|
| 622 |
-
if (acInput) {
|
| 623 |
-
acInput.addEventListener('input', () => updateCounts(acInput, acCharCount, null));
|
| 624 |
-
|
| 625 |
-
btnAutocomplete.addEventListener('click', async () => {
|
| 626 |
-
const text = acInput.value;
|
| 627 |
-
if (!text.trim() || text.trim().length < 3) { showToast('اكتب ٣ أحرف على الأقل'); return; }
|
| 628 |
-
|
| 629 |
-
setLoading(true, 'جارٍ الاقتراح...');
|
| 630 |
try {
|
| 631 |
-
const data = await
|
| 632 |
-
const suggestions = data.suggestions || [];
|
| 633 |
-
acResultSection.classList.remove('is-hidden');
|
| 634 |
|
| 635 |
-
if (
|
| 636 |
-
|
| 637 |
return;
|
| 638 |
}
|
| 639 |
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
}
|
|
|
|
|
|
|
| 653 |
} catch (error) {
|
| 654 |
-
console.error('[Bayan SP]
|
| 655 |
-
|
| 656 |
-
} finally {
|
| 657 |
-
setLoading(false);
|
| 658 |
}
|
| 659 |
});
|
| 660 |
-
}
|
| 661 |
-
|
| 662 |
-
// ══════════════════════════════════════════════════════════
|
| 663 |
-
// Phase 5: Download corrected text / summary as .txt
|
| 664 |
-
// Buttons injected programmatically to avoid touching sidepanel.html.
|
| 665 |
-
// ══════════════════════════════════════════════════════════
|
| 666 |
-
function downloadTxt(text, filename) {
|
| 667 |
-
if (!text) { showToast('لا يوجد نص للتنزيل'); return; }
|
| 668 |
-
try {
|
| 669 |
-
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
| 670 |
-
const url = URL.createObjectURL(blob);
|
| 671 |
-
const a = document.createElement('a');
|
| 672 |
-
a.href = url;
|
| 673 |
-
a.download = filename;
|
| 674 |
-
document.body.appendChild(a);
|
| 675 |
-
a.click();
|
| 676 |
-
a.remove();
|
| 677 |
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
| 678 |
-
showToast('✓ تم تنزيل الملف');
|
| 679 |
-
} catch (e) {
|
| 680 |
-
console.error('[Bayan SP] Download error:', e);
|
| 681 |
-
showToast('تعذّر التنزيل');
|
| 682 |
-
}
|
| 683 |
-
}
|
| 684 |
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
btn.type = 'button';
|
| 692 |
-
btn.title = 'تنزيل كملف نصي';
|
| 693 |
-
btn.innerHTML = SP_DOWNLOAD_ICON;
|
| 694 |
-
btn.addEventListener('click', () => downloadTxt((getText() || '').trim(), filename));
|
| 695 |
-
anchorBtn.parentElement.appendChild(btn);
|
| 696 |
-
}
|
| 697 |
|
| 698 |
-
|
| 699 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 700 |
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
|
|
|
| 709 |
|
| 710 |
-
|
| 711 |
-
if (
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
if (!text) { showToast('لا يوجد نص للتطبيق'); return; }
|
| 720 |
-
writeBackToPage(text, 'auto', source, sourceSelectionText);
|
| 721 |
-
});
|
| 722 |
-
anchorBtn.parentElement.appendChild(btn);
|
| 723 |
}
|
| 724 |
|
| 725 |
-
addApplyToPageButton(btnCopySummary, () => summaryText.textContent, 'summarize');
|
| 726 |
-
if (btnCopyDialect) addApplyToPageButton(btnCopyDialect, () => dialectText.textContent, 'dialect');
|
| 727 |
-
if (btnCopyQuran) addApplyToPageButton(btnCopyQuran, () => quranText.textContent, 'quran');
|
| 728 |
-
|
| 729 |
// ══════════════════════════════════════════════════════════
|
| 730 |
// Status check
|
| 731 |
// ══════════════════════════════════════════════════════════
|
|
@@ -743,17 +840,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 743 |
})();
|
| 744 |
|
| 745 |
// ══════════════════════════════════════════════════════════
|
| 746 |
-
// Context menu pickup
|
| 747 |
-
// NOTE: background.js calls sidePanel.open() BEFORE storage.set()
|
| 748 |
-
// to preserve the user gesture token. This means on first open,
|
| 749 |
-
// storage may not be ready yet. We retry once after 300ms.
|
| 750 |
-
// If the panel is ALREADY open, the storage.onChanged listener
|
| 751 |
-
// below catches new actions in real-time.
|
| 752 |
// ══════════════════════════════════════════════════════════
|
| 753 |
-
|
| 754 |
-
// Dispatch a context action (correct/summarize/dialect/quran) by filling
|
| 755 |
-
// the matching tab's input, switching to it, and auto-running its model.
|
| 756 |
-
// Declared after all element refs so dialect/quran handles are in scope.
|
| 757 |
function runContextAction(action, text) {
|
| 758 |
sourceSelectionText = text;
|
| 759 |
if (action === TAB.CORRECT) {
|
|
@@ -763,12 +851,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 763 |
setTimeout(() => runAnalysis(text), 100);
|
| 764 |
} else if (action === TAB.SUMMARIZE) {
|
| 765 |
summaryInputText.value = text;
|
| 766 |
-
|
|
|
|
| 767 |
document.querySelector(`[data-tab="${TAB.SUMMARIZE}"]`)?.click();
|
| 768 |
setTimeout(() => btnSummarize.click(), 100);
|
| 769 |
} else if (action === TAB.DIALECT && dialectInput && btnDialect) {
|
| 770 |
dialectInput.value = text;
|
| 771 |
-
|
|
|
|
| 772 |
document.querySelector(`[data-tab="${TAB.DIALECT}"]`)?.click();
|
| 773 |
setTimeout(() => btnDialect.click(), 120);
|
| 774 |
} else if (action === TAB.QURAN && quranInput && btnQuran) {
|
|
@@ -789,14 +879,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 789 |
try {
|
| 790 |
const data = await storage.get(['contextAction', 'contextText', 'contextTimestamp']);
|
| 791 |
|
| 792 |
-
// Storage not ready yet — retry once after 300ms
|
| 793 |
if ((!data.contextAction || !data.contextText) && retryCount < 2) {
|
| 794 |
setTimeout(() => tryPickupContext(retryCount + 1), 300);
|
| 795 |
return;
|
| 796 |
}
|
| 797 |
|
| 798 |
if (!data.contextAction || !data.contextText) {
|
| 799 |
-
// No context action after retries — restore previous state
|
| 800 |
await restoreState();
|
| 801 |
return;
|
| 802 |
}
|
|
@@ -809,11 +897,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 809 |
}
|
| 810 |
|
| 811 |
contextConsumed = true;
|
| 812 |
-
|
| 813 |
console.log(`[Bayan SP] Context action: ${data.contextAction}, text: ${data.contextText.length} chars`);
|
| 814 |
-
|
| 815 |
runContextAction(data.contextAction, data.contextText);
|
| 816 |
-
|
| 817 |
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
|
| 818 |
|
| 819 |
} catch (err) {
|
|
@@ -822,7 +907,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 822 |
}
|
| 823 |
}
|
| 824 |
|
| 825 |
-
// Start context pickup with retry
|
| 826 |
tryPickupContext(0);
|
| 827 |
|
| 828 |
// ══════════════════════════════════════════════════════════
|
|
@@ -839,9 +923,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 839 |
if (!action || !text) return;
|
| 840 |
|
| 841 |
console.log(`[Bayan SP] Storage changed — new context: ${action}, ${text.length} chars`);
|
| 842 |
-
|
| 843 |
runContextAction(action, text);
|
| 844 |
-
|
| 845 |
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
|
| 846 |
});
|
| 847 |
}
|
|
@@ -851,21 +933,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 851 |
// ── Theme Toggle Logic ──
|
| 852 |
(function initBayanThemeToggle() {
|
| 853 |
const toggleBtn = document.getElementById('ext-theme-toggle');
|
| 854 |
-
|
| 855 |
-
// Load theme from storage
|
| 856 |
chrome.storage.local.get(['theme'], (result) => {
|
| 857 |
-
const currentTheme = result.theme || 'dark';
|
| 858 |
document.documentElement.setAttribute('data-theme', currentTheme);
|
| 859 |
});
|
| 860 |
|
| 861 |
-
// Sync theme changes instantly across all views
|
| 862 |
chrome.storage.onChanged.addListener((changes, namespace) => {
|
| 863 |
if (namespace === 'local' && changes.theme) {
|
| 864 |
document.documentElement.setAttribute('data-theme', changes.theme.newValue);
|
| 865 |
}
|
| 866 |
});
|
| 867 |
|
| 868 |
-
|
| 869 |
if (toggleBtn) {
|
| 870 |
toggleBtn.addEventListener('click', () => {
|
| 871 |
let theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
|
|
| 1 |
/**
|
| 2 |
* Bayan Chrome Extension — Side Panel Logic
|
| 3 |
*
|
| 4 |
+
* Persistent workspace panel — reuses shared modules.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
*
|
| 6 |
* Key differences from popup.js:
|
| 7 |
* - Persistent: panel stays open across page navigations
|
| 8 |
* - Auto-analysis: text injected from context menu auto-analyzes
|
| 9 |
* - Debounced live updates: re-analyzes on user edits (500ms debounce)
|
| 10 |
+
* - Write-back-to-page: can apply corrections/results to the source field
|
| 11 |
* - State persistence: last analysis saved to chrome.storage.session
|
| 12 |
*/
|
| 13 |
|
|
|
|
| 23 |
const btnClear = document.getElementById('btn-clear');
|
| 24 |
const btnApplyAll = document.getElementById('btn-apply-all');
|
| 25 |
const btnApplyPage = document.getElementById('btn-apply-page');
|
| 26 |
+
const btnCopyText = document.getElementById('btn-copy-text');
|
| 27 |
const scoreSection = document.getElementById('score-section');
|
|
|
|
|
|
|
| 28 |
const suggestionsSection = document.getElementById('suggestions-section');
|
| 29 |
const suggestionsList = document.getElementById('suggestions-list');
|
| 30 |
const timingSection = document.getElementById('timing-section');
|
|
|
|
| 34 |
|
| 35 |
// Summary tab
|
| 36 |
const summaryInputText = document.getElementById('summary-input-text');
|
| 37 |
+
const summaryWordCountInput = document.getElementById('summary-word-count-input');
|
| 38 |
const btnSummarize = document.getElementById('btn-summarize');
|
| 39 |
const summaryResultSection = document.getElementById('summary-result-section');
|
| 40 |
const summaryText = document.getElementById('summary-text');
|
| 41 |
+
const summaryStats = document.getElementById('summary-stats');
|
| 42 |
+
const summaryWordCount = document.getElementById('summary-word-count');
|
| 43 |
+
const summaryCompression = document.getElementById('summary-compression');
|
| 44 |
const btnCopySummary = document.getElementById('btn-copy-summary');
|
| 45 |
|
| 46 |
// Score
|
|
|
|
| 59 |
let isStale = false;
|
| 60 |
let isAnalyzing = false;
|
| 61 |
let contextConsumed = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
let sourceSelectionText = '';
|
| 63 |
+
let summaryMode = 'paragraph';
|
| 64 |
|
| 65 |
const SCORE_CIRCUMFERENCE = 440;
|
|
|
|
| 66 |
|
| 67 |
// ══════════════════════════════════════════════════════════
|
| 68 |
// Tab switching
|
|
|
|
| 77 |
document.querySelectorAll('.sp-panel').forEach((p) => {
|
| 78 |
p.classList.toggle('active', p.id === `panel-${targetTab}`);
|
| 79 |
});
|
| 80 |
+
saveState();
|
| 81 |
});
|
| 82 |
});
|
| 83 |
|
|
|
|
| 94 |
|
| 95 |
inputText.addEventListener('input', () => {
|
| 96 |
updateCounts(inputText, charCount, wordCount);
|
| 97 |
+
saveState();
|
| 98 |
|
|
|
|
| 99 |
if (currentSuggestions.length > 0 && inputText.value !== analyzedText) {
|
| 100 |
markStale();
|
| 101 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
});
|
| 103 |
|
| 104 |
summaryInputText.addEventListener('input', () => {
|
| 105 |
+
const text = summaryInputText.value.trim();
|
| 106 |
+
const words = text ? text.split(/\s+/).length : 0;
|
| 107 |
+
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
|
| 108 |
+
saveState();
|
| 109 |
});
|
| 110 |
|
| 111 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 114 |
function markStale() {
|
| 115 |
if (isStale) return;
|
| 116 |
isStale = true;
|
|
|
|
| 117 |
if (suggestionsSection) suggestionsSection.classList.add('sp-stale');
|
| 118 |
+
showToast('⚠ النص تغيّر — أعد التحليل لتحديث الاقتراحات', 4000);
|
| 119 |
+
btnCorrect.innerHTML = `
|
| 120 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h5M20 20v-5h-5M4 9a8 8 0 0114-3M20 15a8 8 0 01-14 3"/></svg>
|
| 121 |
+
إعادة التحليل`;
|
| 122 |
}
|
| 123 |
|
| 124 |
function clearStale() {
|
| 125 |
isStale = false;
|
|
|
|
| 126 |
if (suggestionsSection) suggestionsSection.classList.remove('sp-stale');
|
| 127 |
+
btnCorrect.innerHTML = `
|
| 128 |
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4"/></svg>
|
| 129 |
+
تحليل وتصحيح`;
|
| 130 |
}
|
| 131 |
|
| 132 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 147 |
|
| 148 |
// ══════════════════════════════════════════════════════════
|
| 149 |
// Write-back to the page field (panel → background → content script)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
// ══════════════════════════════════════════════════════════
|
| 151 |
function writeBackToPage(text, mode = 'auto', source = 'correct', find = '') {
|
| 152 |
try {
|
|
|
|
| 192 |
|
| 193 |
if (!suggestions || suggestions.length === 0) {
|
| 194 |
suggestionsSection.classList.add('is-hidden');
|
|
|
|
| 195 |
return;
|
| 196 |
}
|
| 197 |
|
| 198 |
suggestionsSection.classList.remove('is-hidden');
|
| 199 |
suggestionsList.innerHTML = suggestions.map((s, i) => buildSuggestionCardHTML(s, i)).join('');
|
| 200 |
|
|
|
|
| 201 |
suggestionsList.querySelectorAll('.bayan-alt-chip').forEach((chip) => {
|
| 202 |
chip.addEventListener('click', (e) => {
|
| 203 |
e.stopPropagation();
|
|
|
|
| 225 |
const counts = countByType(currentSuggestions);
|
| 226 |
updateScore(counts.spelling, counts.grammar, counts.punctuation);
|
| 227 |
renderSuggestions(currentSuggestions);
|
|
|
|
| 228 |
saveState();
|
| 229 |
showToast('✓ تم التصحيح');
|
| 230 |
});
|
|
|
|
| 237 |
// Core analysis function
|
| 238 |
// ══════════════════════════════════════════════════════════
|
| 239 |
async function runAnalysis(text) {
|
| 240 |
+
if (!text || text.trim().split(/\s+/).length < 2) {
|
| 241 |
+
showToast('أدخل كلمتين على الأقل');
|
| 242 |
return;
|
| 243 |
}
|
| 244 |
if (text.length > CONFIG.MAX_ANALYZE_LENGTH) {
|
|
|
|
| 261 |
inputText.value = analyzedText;
|
| 262 |
updateCounts(inputText, charCount, wordCount);
|
| 263 |
|
|
|
|
|
|
|
|
|
|
| 264 |
const counts = countByType(suggestions);
|
| 265 |
updateScore(counts.spelling, counts.grammar, counts.punctuation);
|
| 266 |
renderSuggestions(suggestions);
|
|
|
|
| 297 |
function saveState() {
|
| 298 |
const storage = getStorage();
|
| 299 |
if (!storage) return;
|
| 300 |
+
|
| 301 |
+
const activeTabEl = document.querySelector('.sp-tab.active');
|
| 302 |
+
const activeTab = activeTabEl ? activeTabEl.dataset.tab : 'correct';
|
| 303 |
+
|
| 304 |
storage.set({
|
| 305 |
spLastText: analyzedText,
|
| 306 |
spLastSuggestions: currentSuggestions,
|
| 307 |
spLastInput: inputText.value,
|
| 308 |
+
spLastSummarize: (document.getElementById('summary-input-text') || {}).value || '',
|
| 309 |
+
spLastDialect: (document.getElementById('dialect-input-text') || {}).value || '',
|
| 310 |
+
spLastQuran: (document.getElementById('quran-input-text') || {}).value || '',
|
| 311 |
+
spActiveTab: activeTab
|
| 312 |
}).catch(() => {});
|
| 313 |
}
|
| 314 |
|
|
|
|
| 317 |
if (!storage) return false;
|
| 318 |
|
| 319 |
try {
|
| 320 |
+
const data = await storage.get(['spLastText', 'spLastSuggestions', 'spLastInput', 'spLastSummarize', 'spLastDialect', 'spLastQuran', 'spActiveTab']);
|
| 321 |
+
|
| 322 |
+
if (data.spLastSummarize) {
|
| 323 |
+
const sInput = document.getElementById('summary-input-text');
|
| 324 |
+
if (sInput) {
|
| 325 |
+
sInput.value = data.spLastSummarize;
|
| 326 |
+
const words = data.spLastSummarize.trim() ? data.spLastSummarize.trim().split(/\s+/).length : 0;
|
| 327 |
+
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
if (data.spLastDialect) {
|
| 331 |
+
const dInput = document.getElementById('dialect-input-text');
|
| 332 |
+
if (dInput) { dInput.value = data.spLastDialect; updateCounts(dInput, document.getElementById('dialect-char-count'), null); }
|
| 333 |
+
}
|
| 334 |
+
if (data.spLastQuran) {
|
| 335 |
+
const qInput = document.getElementById('quran-input-text');
|
| 336 |
+
if (qInput) { qInput.value = data.spLastQuran; updateCounts(qInput, document.getElementById('quran-char-count'), null); }
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
if (data.spActiveTab) {
|
| 340 |
+
const tabBtn = document.querySelector(`.sp-tab[data-tab="${data.spActiveTab}"]`);
|
| 341 |
+
if (tabBtn) tabBtn.click();
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
if (!data.spLastText || !data.spLastSuggestions) return false;
|
| 345 |
|
| 346 |
analyzedText = data.spLastText;
|
|
|
|
| 348 |
inputText.value = data.spLastInput || analyzedText;
|
| 349 |
updateCounts(inputText, charCount, wordCount);
|
| 350 |
|
|
|
|
|
|
|
|
|
|
| 351 |
const counts = countByType(currentSuggestions);
|
| 352 |
updateScore(counts.spelling, counts.grammar, counts.punctuation);
|
| 353 |
renderSuggestions(currentSuggestions);
|
|
|
|
| 372 |
sourceSelectionText = '';
|
| 373 |
updateCounts(inputText, charCount, wordCount);
|
| 374 |
scoreSection.classList.add('is-hidden');
|
|
|
|
| 375 |
suggestionsSection.classList.add('is-hidden');
|
| 376 |
timingSection.classList.add('is-hidden');
|
| 377 |
currentSuggestions = [];
|
|
|
|
| 391 |
inputText.value = analyzedText;
|
| 392 |
updateCounts(inputText, charCount, wordCount);
|
| 393 |
currentSuggestions = [];
|
|
|
|
| 394 |
updateScore(0, 0, 0);
|
| 395 |
renderSuggestions([]);
|
| 396 |
saveState();
|
|
|
|
| 398 |
showToast('✓ تم تطبيق جميع التصحيحات');
|
| 399 |
});
|
| 400 |
|
|
|
|
|
|
|
|
|
|
| 401 |
if (btnApplyPage) {
|
| 402 |
btnApplyPage.addEventListener('click', () => {
|
| 403 |
+
if (!analyzedText && !inputText.value.trim()) { showToast('لا يوجد نص للتطبيق'); return; }
|
| 404 |
if (isStale) { showToast('⚠ أعد التحليل أولاً — النص تغيّر'); return; }
|
| 405 |
const finalText = currentSuggestions.length > 0
|
| 406 |
? applyAllPatches(analyzedText, currentSuggestions)
|
| 407 |
+
: (analyzedText || inputText.value.trim());
|
| 408 |
writeBackToPage(finalText, 'auto', 'correct', sourceSelectionText);
|
| 409 |
});
|
| 410 |
}
|
| 411 |
|
| 412 |
+
if (btnCopyText) {
|
| 413 |
+
btnCopyText.addEventListener('click', () => {
|
| 414 |
+
const text = inputText.value || '';
|
| 415 |
+
navigator.clipboard.writeText(text)
|
| 416 |
+
.then(() => showToast('✓ تم نسخ النص'))
|
| 417 |
+
.catch(() => showToast('تعذّر النسخ'));
|
| 418 |
+
});
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// ══════════════════════════════════════════════════════════
|
| 422 |
+
// Summary mode toggle (paragraph / bullets)
|
| 423 |
+
// ══════════════════════════════════════════════════════════
|
| 424 |
+
document.querySelectorAll('.sp-mode-btn').forEach((btn) => {
|
| 425 |
+
btn.addEventListener('click', () => {
|
| 426 |
+
summaryMode = btn.dataset.mode;
|
| 427 |
+
document.querySelectorAll('.sp-mode-btn').forEach((b) => {
|
| 428 |
+
b.classList.toggle('active', b.dataset.mode === summaryMode);
|
| 429 |
+
});
|
| 430 |
+
});
|
| 431 |
});
|
| 432 |
|
| 433 |
// ════════════════════════════════���═════════════════════════
|
|
|
|
| 445 |
const data = await bayanSummarize(text, lengthValue);
|
| 446 |
if (data.status === 'success' && data.summary) {
|
| 447 |
summaryResultSection.classList.remove('is-hidden');
|
| 448 |
+
|
| 449 |
+
const summaryContent = data.summary;
|
| 450 |
+
|
| 451 |
+
if (summaryMode === 'bullets') {
|
| 452 |
+
const sentences = summaryContent.split(/[.،؛]\s*/).filter(s => s.trim().length > 2);
|
| 453 |
+
const ul = document.createElement('ul');
|
| 454 |
+
ul.style.cssText = 'list-style: disc; padding-right: 1.5rem; direction: rtl; text-align: right;';
|
| 455 |
+
sentences.forEach(s => {
|
| 456 |
+
const li = document.createElement('li');
|
| 457 |
+
li.textContent = s.trim();
|
| 458 |
+
li.style.marginBottom = '8px';
|
| 459 |
+
ul.appendChild(li);
|
| 460 |
+
});
|
| 461 |
+
summaryText.textContent = '';
|
| 462 |
+
summaryText.appendChild(ul);
|
| 463 |
+
} else {
|
| 464 |
+
summaryText.textContent = summaryContent;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
const origWords = text.trim().split(/\s+/).length;
|
| 468 |
+
const sumWords = summaryContent.trim().split(/\s+/).length;
|
| 469 |
+
const compression = origWords > 0 ? Math.round((1 - sumWords / origWords) * 100) : 0;
|
| 470 |
+
|
| 471 |
+
if (summaryStats) {
|
| 472 |
+
summaryStats.classList.remove('is-hidden');
|
| 473 |
+
if (summaryWordCount) summaryWordCount.textContent = sumWords.toLocaleString('ar-EG');
|
| 474 |
+
if (summaryCompression) summaryCompression.textContent = compression.toLocaleString('ar-EG') + '٪';
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
showToast('✓ تم التلخيص');
|
| 478 |
} else {
|
| 479 |
showToast('تعذّر التلخيص — حاول مرة أخرى');
|
|
|
|
| 487 |
});
|
| 488 |
|
| 489 |
btnCopySummary.addEventListener('click', () => {
|
| 490 |
+
const text = summaryText.innerText || summaryText.textContent || '';
|
| 491 |
navigator.clipboard.writeText(text)
|
| 492 |
.then(() => showToast('✓ تم نسخ الملخص'))
|
| 493 |
.catch(() => showToast('تعذّر النسخ'));
|
| 494 |
});
|
| 495 |
|
| 496 |
+
const btnApplySummary = document.getElementById('btn-apply-summary');
|
| 497 |
+
if (btnApplySummary) {
|
| 498 |
+
btnApplySummary.addEventListener('click', () => {
|
| 499 |
+
const text = summaryText.innerText || summaryText.textContent || '';
|
| 500 |
+
if (!text) { showToast('لا يوجد ملخص للتطبيق'); return; }
|
| 501 |
+
writeBackToPage(text, 'auto', 'summarize', sourceSelectionText);
|
| 502 |
+
});
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
// ══════════════════════════════════════════════════════════
|
| 506 |
+
// Summary: File import (.txt / .docx)
|
| 507 |
+
// ══════════════════════════════════════════════════════════
|
| 508 |
+
const summaryImportInput = document.getElementById('summary-import-input');
|
| 509 |
+
if (summaryImportInput) {
|
| 510 |
+
summaryImportInput.addEventListener('change', (e) => {
|
| 511 |
+
const file = e.target.files && e.target.files[0];
|
| 512 |
+
if (!file) return;
|
| 513 |
+
|
| 514 |
+
if (file.name.endsWith('.txt')) {
|
| 515 |
+
const reader = new FileReader();
|
| 516 |
+
reader.onload = (ev) => {
|
| 517 |
+
summaryInputText.value = ev.target.result;
|
| 518 |
+
const words = summaryInputText.value.trim() ? summaryInputText.value.trim().split(/\s+/).length : 0;
|
| 519 |
+
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
|
| 520 |
+
showToast('✓ تم استيراد الملف');
|
| 521 |
+
saveState();
|
| 522 |
+
};
|
| 523 |
+
reader.readAsText(file, 'UTF-8');
|
| 524 |
+
} else if (file.name.endsWith('.docx')) {
|
| 525 |
+
if (typeof mammoth !== 'undefined') {
|
| 526 |
+
const reader = new FileReader();
|
| 527 |
+
reader.onload = (ev) => {
|
| 528 |
+
mammoth.extractRawText({ arrayBuffer: ev.target.result })
|
| 529 |
+
.then((result) => {
|
| 530 |
+
summaryInputText.value = result.value;
|
| 531 |
+
const words = result.value.trim() ? result.value.trim().split(/\s+/).length : 0;
|
| 532 |
+
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
|
| 533 |
+
showToast('✓ تم استيراد الملف');
|
| 534 |
+
saveState();
|
| 535 |
+
})
|
| 536 |
+
.catch(() => showToast('خطأ في قراءة الملف'));
|
| 537 |
+
};
|
| 538 |
+
reader.readAsArrayBuffer(file);
|
| 539 |
+
} else {
|
| 540 |
+
showToast('صيغة .docx غير مدعومة — استخدم .txt');
|
| 541 |
+
}
|
| 542 |
+
}
|
| 543 |
+
e.target.value = '';
|
| 544 |
+
});
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
// ════════════════════════════════════════════════���═════════
|
| 548 |
+
// Summary: Export dropdown (.txt / .docx / .pdf)
|
| 549 |
+
// ══════════════════════════════════════════════════════════
|
| 550 |
+
const btnExportSummary = document.getElementById('btn-export-summary');
|
| 551 |
+
const summaryExportMenu = document.getElementById('summary-export-menu');
|
| 552 |
+
|
| 553 |
+
if (btnExportSummary && summaryExportMenu) {
|
| 554 |
+
btnExportSummary.addEventListener('click', (e) => {
|
| 555 |
+
e.stopPropagation();
|
| 556 |
+
summaryExportMenu.classList.toggle('is-hidden');
|
| 557 |
+
});
|
| 558 |
+
|
| 559 |
+
document.addEventListener('click', () => {
|
| 560 |
+
summaryExportMenu.classList.add('is-hidden');
|
| 561 |
+
});
|
| 562 |
+
|
| 563 |
+
summaryExportMenu.addEventListener('click', (e) => {
|
| 564 |
+
e.stopPropagation();
|
| 565 |
+
});
|
| 566 |
+
|
| 567 |
+
summaryExportMenu.querySelectorAll('.sp-export-item').forEach((item) => {
|
| 568 |
+
item.addEventListener('click', () => {
|
| 569 |
+
const format = item.dataset.format;
|
| 570 |
+
const text = (summaryText.innerText || summaryText.textContent || '').trim();
|
| 571 |
+
if (!text) { showToast('لا يوجد ملخص للتصدير'); return; }
|
| 572 |
+
|
| 573 |
+
summaryExportMenu.classList.add('is-hidden');
|
| 574 |
+
exportSummary(format, text);
|
| 575 |
+
});
|
| 576 |
+
});
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
function exportSummary(format, text) {
|
| 580 |
+
if (format === 'txt') {
|
| 581 |
+
downloadFile(text, 'ملخص-بيان.txt', 'text/plain;charset=utf-8');
|
| 582 |
+
showToast('✓ تم تصدير الملخص');
|
| 583 |
+
} else if (format === 'docx') {
|
| 584 |
+
if (typeof docx === 'undefined') { showToast('مكتبة Word غير محمّلة'); return; }
|
| 585 |
+
try {
|
| 586 |
+
const paragraphs = text.split(/\n+/).filter(p => p.trim());
|
| 587 |
+
const children = paragraphs.map(block =>
|
| 588 |
+
new docx.Paragraph({
|
| 589 |
+
bidirectional: true,
|
| 590 |
+
alignment: docx.AlignmentType.RIGHT,
|
| 591 |
+
children: [new docx.TextRun({ text: block, rightToLeft: true, font: 'Arial' })]
|
| 592 |
+
})
|
| 593 |
+
);
|
| 594 |
+
const doc = new docx.Document({ sections: [{ properties: { rightToLeft: true }, children }] });
|
| 595 |
+
docx.Packer.toBlob(doc).then((blob) => {
|
| 596 |
+
downloadBlob(blob, 'ملخص-بيان.docx');
|
| 597 |
+
showToast('✓ تم تصدير الملخص كـ Word');
|
| 598 |
+
}).catch(() => showToast('تعذر تصدير ملف Word'));
|
| 599 |
+
} catch { showToast('تعذر تصدير ملف Word'); }
|
| 600 |
+
} else if (format === 'pdf') {
|
| 601 |
+
if (typeof html2pdf === 'undefined') { showToast('مكتبة PDF غير محمّلة'); return; }
|
| 602 |
+
showToast('جاري تصدير PDF...');
|
| 603 |
+
const html = '<div dir="rtl" style="font-family:Arial,sans-serif;font-size:16px;line-height:2;text-align:right;padding:20px;">' +
|
| 604 |
+
text.split(/\n+/).map(p => '<p>' + p + '</p>').join('') + '</div>';
|
| 605 |
+
html2pdf().set({
|
| 606 |
+
margin: [15, 15, 15, 15],
|
| 607 |
+
filename: 'ملخص-بيان.pdf',
|
| 608 |
+
image: { type: 'jpeg', quality: 0.95 },
|
| 609 |
+
html2canvas: { scale: 1.5, useCORS: true, backgroundColor: '#ffffff' },
|
| 610 |
+
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
|
| 611 |
+
}).from(html, 'string').save()
|
| 612 |
+
.then(() => showToast('✓ تم تصدير الملخص كـ PDF'))
|
| 613 |
+
.catch(() => showToast('تعذر تصدير PDF'));
|
| 614 |
+
}
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
function downloadFile(text, filename, mime) {
|
| 618 |
+
const blob = new Blob([text], { type: mime });
|
| 619 |
+
downloadBlob(blob, filename);
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
function downloadBlob(blob, filename) {
|
| 623 |
+
const url = URL.createObjectURL(blob);
|
| 624 |
+
const a = document.createElement('a');
|
| 625 |
+
a.href = url;
|
| 626 |
+
a.download = filename;
|
| 627 |
+
document.body.appendChild(a);
|
| 628 |
+
a.click();
|
| 629 |
+
a.remove();
|
| 630 |
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
// ══════════════════════════════════════════════════════════
|
| 634 |
// Dialect → MSA conversion
|
| 635 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 641 |
const btnCopyDialect = document.getElementById('btn-copy-dialect');
|
| 642 |
|
| 643 |
if (dialectInput) {
|
| 644 |
+
dialectInput.addEventListener('input', () => {
|
| 645 |
+
const chars = dialectInput.value.length;
|
| 646 |
+
if (dialectCharCount) dialectCharCount.textContent = chars.toLocaleString('ar-EG');
|
| 647 |
+
saveState();
|
| 648 |
+
});
|
| 649 |
|
| 650 |
btnDialect.addEventListener('click', async () => {
|
| 651 |
const text = dialectInput.value.trim();
|
|
|
|
| 674 |
.then(() => showToast('✓ تم نسخ النص'))
|
| 675 |
.catch(() => showToast('تعذّر النسخ'));
|
| 676 |
});
|
| 677 |
+
|
| 678 |
+
const btnApplyDialect = document.getElementById('btn-apply-dialect');
|
| 679 |
+
if (btnApplyDialect) {
|
| 680 |
+
btnApplyDialect.addEventListener('click', () => {
|
| 681 |
+
const text = dialectText.textContent || '';
|
| 682 |
+
if (!text) { showToast('لا يوجد نص للتطبيق'); return; }
|
| 683 |
+
writeBackToPage(text, 'auto', 'dialect', sourceSelectionText);
|
| 684 |
+
});
|
| 685 |
+
}
|
| 686 |
}
|
| 687 |
|
| 688 |
// ══════════════════════════════════════════════════════════
|
| 689 |
+
// Quran verification + translation (matching popup)
|
| 690 |
// ══════════════════════════════════════════════════════════
|
| 691 |
const quranInput = document.getElementById('quran-input-text');
|
| 692 |
const quranCharCount = document.getElementById('quran-char-count');
|
| 693 |
const btnQuran = document.getElementById('btn-quran');
|
| 694 |
const quranResultSection = document.getElementById('quran-result-section');
|
| 695 |
+
const quranUthmani = document.getElementById('quran-uthmani');
|
| 696 |
+
const quranReference = document.getElementById('quran-reference');
|
| 697 |
const btnCopyQuran = document.getElementById('btn-copy-quran');
|
|
|
|
|
|
|
| 698 |
const quranLangSelect = document.getElementById('quran-lang-select');
|
| 699 |
+
const quranTransSection = document.getElementById('quran-translation-section');
|
| 700 |
+
const quranTransText = document.getElementById('quran-trans-text');
|
| 701 |
+
const quranTransRef = document.getElementById('quran-trans-ref');
|
| 702 |
+
|
| 703 |
+
let _quranQuery = '';
|
| 704 |
+
let _quranVerse = '';
|
| 705 |
+
let _quranRef = '';
|
| 706 |
+
let _quranTransText = '';
|
| 707 |
+
let _quranTransRef = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
|
| 709 |
if (quranInput) {
|
| 710 |
+
quranInput.addEventListener('input', () => { updateCounts(quranInput, quranCharCount, null); saveState(); });
|
| 711 |
|
| 712 |
btnQuran.addEventListener('click', async () => {
|
| 713 |
const text = quranInput.value.trim();
|
| 714 |
if (!text) { showToast('أدخل آية للتدقيق'); return; }
|
| 715 |
|
| 716 |
+
_quranQuery = text;
|
| 717 |
setLoading(true, 'جارٍ التدقيق...');
|
| 718 |
+
quranTransSection.classList.add('is-hidden');
|
| 719 |
+
quranLangSelect.value = '';
|
| 720 |
+
|
| 721 |
try {
|
| 722 |
const data = await bayanQuran(text);
|
| 723 |
quranResultSection.classList.remove('is-hidden');
|
| 724 |
+
|
|
|
|
|
|
|
|
|
|
| 725 |
if (data.error) {
|
| 726 |
+
quranUthmani.textContent = data.error;
|
| 727 |
+
quranReference.textContent = '';
|
| 728 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
}
|
| 730 |
+
|
| 731 |
+
const seg = data.matched_segment || '';
|
| 732 |
+
const refMatch = seg.match(/【([^】]+)】/);
|
| 733 |
+
const verseText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 734 |
+
const reference = refMatch ? refMatch[1] : '';
|
| 735 |
+
|
| 736 |
+
_quranVerse = verseText;
|
| 737 |
+
_quranRef = reference;
|
| 738 |
+
quranUthmani.textContent = verseText;
|
| 739 |
+
quranReference.textContent = reference ? `[${reference}]` : '';
|
| 740 |
+
showToast('✓ تم التدقيق');
|
| 741 |
} catch (error) {
|
| 742 |
console.error('[Bayan SP] Quran error:', error);
|
| 743 |
+
quranResultSection.classList.remove('is-hidden');
|
| 744 |
+
quranUthmani.textContent = 'خطأ في الاتصال — تحقق من الإنترنت';
|
| 745 |
+
quranReference.textContent = '';
|
| 746 |
} finally {
|
| 747 |
setLoading(false);
|
| 748 |
}
|
| 749 |
});
|
| 750 |
|
| 751 |
+
quranLangSelect.addEventListener('change', async () => {
|
| 752 |
+
const lang = quranLangSelect.value;
|
| 753 |
+
if (!lang || !_quranQuery) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
|
| 755 |
+
quranTransSection.classList.remove('is-hidden');
|
| 756 |
+
quranTransText.textContent = '⏳ جاري الترجمة...';
|
| 757 |
+
if (quranTransRef) quranTransRef.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
try {
|
| 760 |
+
const data = await bayanQuran(_quranQuery, lang);
|
|
|
|
|
|
|
| 761 |
|
| 762 |
+
if (data.error) {
|
| 763 |
+
quranTransText.textContent = data.error;
|
| 764 |
return;
|
| 765 |
}
|
| 766 |
|
| 767 |
+
const seg = data.matched_segment || '';
|
| 768 |
+
const refMatch = seg.match(/【([^】]+)】/);
|
| 769 |
+
const transText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 770 |
+
const transRef = refMatch ? refMatch[1] : '';
|
| 771 |
+
|
| 772 |
+
_quranTransText = transText;
|
| 773 |
+
_quranTransRef = transRef;
|
| 774 |
+
|
| 775 |
+
quranTransText.textContent = transText;
|
| 776 |
+
if (quranTransRef && transRef) {
|
| 777 |
+
quranTransRef.textContent = `[${transRef}]`;
|
| 778 |
+
quranTransRef.style.display = '';
|
| 779 |
+
}
|
| 780 |
+
const transActions = document.getElementById('quran-trans-actions');
|
| 781 |
+
if (transActions) transActions.style.display = 'flex';
|
| 782 |
} catch (error) {
|
| 783 |
+
console.error('[Bayan SP] Quran translation error:', error);
|
| 784 |
+
quranTransText.textContent = 'حدث خطأ في الترجمة';
|
|
|
|
|
|
|
| 785 |
}
|
| 786 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
|
| 788 |
+
btnCopyQuran.addEventListener('click', () => {
|
| 789 |
+
const text = (_quranVerse || '') + (_quranRef ? ` [${_quranRef}]` : '');
|
| 790 |
+
navigator.clipboard.writeText(text)
|
| 791 |
+
.then(() => showToast('✓ تم النسخ'))
|
| 792 |
+
.catch(() => showToast('تعذّر النسخ'));
|
| 793 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 794 |
|
| 795 |
+
const btnApplyQuran = document.getElementById('btn-apply-quran');
|
| 796 |
+
if (btnApplyQuran) {
|
| 797 |
+
btnApplyQuran.addEventListener('click', () => {
|
| 798 |
+
const verse = _quranVerse || '';
|
| 799 |
+
if (!verse) { showToast('لا يوجد نص قرآني للتطبيق'); return; }
|
| 800 |
+
const textWithRef = verse + (_quranRef ? ` [${_quranRef}]` : '');
|
| 801 |
+
writeBackToPage(textWithRef, 'auto', 'quran', sourceSelectionText);
|
| 802 |
+
});
|
| 803 |
+
}
|
| 804 |
|
| 805 |
+
const btnCopyQuranTrans = document.getElementById('btn-copy-quran-trans');
|
| 806 |
+
if (btnCopyQuranTrans) {
|
| 807 |
+
btnCopyQuranTrans.addEventListener('click', () => {
|
| 808 |
+
const text = (_quranTransText || '') + (_quranTransRef ? ` [${_quranTransRef}]` : '');
|
| 809 |
+
navigator.clipboard.writeText(text)
|
| 810 |
+
.then(() => showToast('✓ تم نسخ الترجمة'))
|
| 811 |
+
.catch(() => showToast('تعذّر النسخ'));
|
| 812 |
+
});
|
| 813 |
+
}
|
| 814 |
|
| 815 |
+
const btnApplyQuranTrans = document.getElementById('btn-apply-quran-trans');
|
| 816 |
+
if (btnApplyQuranTrans) {
|
| 817 |
+
btnApplyQuranTrans.addEventListener('click', () => {
|
| 818 |
+
const text = _quranTransText || '';
|
| 819 |
+
if (!text) { showToast('لا يوجد ترجمة للتطبيق'); return; }
|
| 820 |
+
const textWithRef = text + (_quranTransRef ? ` [${_quranTransRef}]` : '');
|
| 821 |
+
writeBackToPage(textWithRef, 'auto', 'quran', sourceSelectionText);
|
| 822 |
+
});
|
| 823 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 824 |
}
|
| 825 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
// ══════════════════════════════════════════════════════════
|
| 827 |
// Status check
|
| 828 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 840 |
})();
|
| 841 |
|
| 842 |
// ══════════════════════════════════════════════════════════
|
| 843 |
+
// Context menu pickup
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 844 |
// ══════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
|
| 845 |
function runContextAction(action, text) {
|
| 846 |
sourceSelectionText = text;
|
| 847 |
if (action === TAB.CORRECT) {
|
|
|
|
| 851 |
setTimeout(() => runAnalysis(text), 100);
|
| 852 |
} else if (action === TAB.SUMMARIZE) {
|
| 853 |
summaryInputText.value = text;
|
| 854 |
+
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
|
| 855 |
+
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
|
| 856 |
document.querySelector(`[data-tab="${TAB.SUMMARIZE}"]`)?.click();
|
| 857 |
setTimeout(() => btnSummarize.click(), 100);
|
| 858 |
} else if (action === TAB.DIALECT && dialectInput && btnDialect) {
|
| 859 |
dialectInput.value = text;
|
| 860 |
+
const chars = text.length;
|
| 861 |
+
if (dialectCharCount) dialectCharCount.textContent = chars.toLocaleString('ar-EG');
|
| 862 |
document.querySelector(`[data-tab="${TAB.DIALECT}"]`)?.click();
|
| 863 |
setTimeout(() => btnDialect.click(), 120);
|
| 864 |
} else if (action === TAB.QURAN && quranInput && btnQuran) {
|
|
|
|
| 879 |
try {
|
| 880 |
const data = await storage.get(['contextAction', 'contextText', 'contextTimestamp']);
|
| 881 |
|
|
|
|
| 882 |
if ((!data.contextAction || !data.contextText) && retryCount < 2) {
|
| 883 |
setTimeout(() => tryPickupContext(retryCount + 1), 300);
|
| 884 |
return;
|
| 885 |
}
|
| 886 |
|
| 887 |
if (!data.contextAction || !data.contextText) {
|
|
|
|
| 888 |
await restoreState();
|
| 889 |
return;
|
| 890 |
}
|
|
|
|
| 897 |
}
|
| 898 |
|
| 899 |
contextConsumed = true;
|
|
|
|
| 900 |
console.log(`[Bayan SP] Context action: ${data.contextAction}, text: ${data.contextText.length} chars`);
|
|
|
|
| 901 |
runContextAction(data.contextAction, data.contextText);
|
|
|
|
| 902 |
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
|
| 903 |
|
| 904 |
} catch (err) {
|
|
|
|
| 907 |
}
|
| 908 |
}
|
| 909 |
|
|
|
|
| 910 |
tryPickupContext(0);
|
| 911 |
|
| 912 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 923 |
if (!action || !text) return;
|
| 924 |
|
| 925 |
console.log(`[Bayan SP] Storage changed — new context: ${action}, ${text.length} chars`);
|
|
|
|
| 926 |
runContextAction(action, text);
|
|
|
|
| 927 |
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
|
| 928 |
});
|
| 929 |
}
|
|
|
|
| 933 |
// ── Theme Toggle Logic ──
|
| 934 |
(function initBayanThemeToggle() {
|
| 935 |
const toggleBtn = document.getElementById('ext-theme-toggle');
|
| 936 |
+
|
|
|
|
| 937 |
chrome.storage.local.get(['theme'], (result) => {
|
| 938 |
+
const currentTheme = result.theme || 'dark';
|
| 939 |
document.documentElement.setAttribute('data-theme', currentTheme);
|
| 940 |
});
|
| 941 |
|
|
|
|
| 942 |
chrome.storage.onChanged.addListener((changes, namespace) => {
|
| 943 |
if (namespace === 'local' && changes.theme) {
|
| 944 |
document.documentElement.setAttribute('data-theme', changes.theme.newValue);
|
| 945 |
}
|
| 946 |
});
|
| 947 |
|
|
|
|
| 948 |
if (toggleBtn) {
|
| 949 |
toggleBtn.addEventListener('click', () => {
|
| 950 |
let theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
extension/tests/api_response_tc2.json
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"corrected": "أنا ذاهب الي البت.",
|
| 3 |
-
"original": "انا ذاهب الي البت",
|
| 4 |
-
"status": "success",
|
| 5 |
-
"suggestions": [
|
| 6 |
-
{
|
| 7 |
-
"alternatives": [
|
| 8 |
-
|
| 9 |
-
],
|
| 10 |
-
"confidence": 0.8,
|
| 11 |
-
"correction": "البت.",
|
| 12 |
-
"end": 17,
|
| 13 |
-
"id": "bad18f97-041d-4487-a780-17fc4082447d",
|
| 14 |
-
"locked": true,
|
| 15 |
-
"original": "البت",
|
| 16 |
-
"priority": 2,
|
| 17 |
-
"start": 13,
|
| 18 |
-
"type": "punctuation"
|
| 19 |
-
},
|
| 20 |
-
{
|
| 21 |
-
"alternatives": [
|
| 22 |
-
|
| 23 |
-
],
|
| 24 |
-
"confidence": 1.0,
|
| 25 |
-
"correction": "أنا",
|
| 26 |
-
"end": 3,
|
| 27 |
-
"id": "1c3d902b-11a9-4350-86e2-a74e66bb0639",
|
| 28 |
-
"locked": true,
|
| 29 |
-
"original": "انا",
|
| 30 |
-
"priority": 1,
|
| 31 |
-
"start": 0,
|
| 32 |
-
"type": "spelling"
|
| 33 |
-
}
|
| 34 |
-
],
|
| 35 |
-
"timing_ms": {
|
| 36 |
-
"grammar_ms": 585,
|
| 37 |
-
"punctuation_ms": 368,
|
| 38 |
-
"spelling_ms": 2585,
|
| 39 |
-
"total_ms": 3540
|
| 40 |
-
}
|
| 41 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/tests/debug_offsets.ps1
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
$t = 'أنا ذاهب الى البت والمدرسه'
|
| 2 |
-
Write-Host "Length: $($t.Length)"
|
| 3 |
-
Write-Host "Chars 10-13: '$($t.Substring(10, 3))'"
|
| 4 |
-
Write-Host "Chars 14-17: '$($t.Substring(14, 3))'"
|
| 5 |
-
Write-Host "Chars 19-26: '$($t.Substring(19, 7))'"
|
| 6 |
-
|
| 7 |
-
# Direct test: substring replacement
|
| 8 |
-
$before = $t.Substring(0, 10)
|
| 9 |
-
$after = $t.Substring(13)
|
| 10 |
-
$result = $before + 'إلى' + $after
|
| 11 |
-
Write-Host "`nReplace [10:13] 'الى' with 'إلى':"
|
| 12 |
-
Write-Host "Result: '$result'"
|
| 13 |
-
Write-Host "Expected: 'أنا ذاهب إلى البت والمدرسه'"
|
| 14 |
-
Write-Host "Match: $($result -eq 'أنا ذاهب إلى البت والمدرسه')"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/tests/test_api_real.ps1
DELETED
|
@@ -1,80 +0,0 @@
|
|
| 1 |
-
$ErrorActionPreference = 'Stop'
|
| 2 |
-
|
| 3 |
-
# Test Case 1: Basic Correction Flow
|
| 4 |
-
Write-Host "═══════════════════════════════════════════════════" -ForegroundColor Cyan
|
| 5 |
-
Write-Host "TEST CASE 1: Basic Correction Flow" -ForegroundColor Cyan
|
| 6 |
-
Write-Host "═══════════════════════════════════════════════════" -ForegroundColor Cyan
|
| 7 |
-
|
| 8 |
-
$inputText = 'انا ذهبت الي المدرسه اليوم'
|
| 9 |
-
Write-Host "`nInput: `"$inputText`""
|
| 10 |
-
|
| 11 |
-
$body = @{ text = $inputText } | ConvertTo-Json -Compress
|
| 12 |
-
$response = Invoke-RestMethod -Uri 'https://bayan10-bayan-api.hf.space/api/analyze' -Method POST -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ContentType 'application/json; charset=utf-8'
|
| 13 |
-
|
| 14 |
-
Write-Host "`n── API Response ──" -ForegroundColor Yellow
|
| 15 |
-
Write-Host "Status: $($response.status)"
|
| 16 |
-
Write-Host "Original: `"$($response.original)`""
|
| 17 |
-
Write-Host "Corrected: `"$($response.corrected)`""
|
| 18 |
-
Write-Host "Suggestions count: $($response.suggestions.Count)"
|
| 19 |
-
|
| 20 |
-
Write-Host "`n── Timing ──" -ForegroundColor Yellow
|
| 21 |
-
Write-Host "Total: $($response.timing_ms.total_ms)ms"
|
| 22 |
-
Write-Host "Spelling: $($response.timing_ms.spelling_ms)ms"
|
| 23 |
-
Write-Host "Grammar: $($response.timing_ms.grammar_ms)ms"
|
| 24 |
-
Write-Host "Punctuation: $($response.timing_ms.punctuation_ms)ms"
|
| 25 |
-
|
| 26 |
-
Write-Host "`n── Suggestions ──" -ForegroundColor Yellow
|
| 27 |
-
foreach ($s in $response.suggestions) {
|
| 28 |
-
Write-Host " [$($s.type)] `"$($s.original)`" → `"$($s.correction)`" (start:$($s.start), end:$($s.end), id:$($s.id.Substring(0,8))...)"
|
| 29 |
-
if ($s.alternatives) {
|
| 30 |
-
Write-Host " Alternatives: $($s.alternatives -join ', ')"
|
| 31 |
-
}
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
# Schema validation
|
| 35 |
-
Write-Host "`n── Schema Validation ──" -ForegroundColor Yellow
|
| 36 |
-
$fields = @('status', 'original', 'corrected', 'suggestions', 'timing_ms')
|
| 37 |
-
foreach ($f in $fields) {
|
| 38 |
-
$exists = $null -ne $response.$f
|
| 39 |
-
$icon = if ($exists) { "✅" } else { "❌" }
|
| 40 |
-
Write-Host " $icon Field '$f' present"
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
# Suggestion schema
|
| 44 |
-
if ($response.suggestions.Count -gt 0) {
|
| 45 |
-
$s = $response.suggestions[0]
|
| 46 |
-
$sFields = @('id', 'start', 'end', 'original', 'correction', 'type', 'alternatives')
|
| 47 |
-
foreach ($f in $sFields) {
|
| 48 |
-
$exists = $null -ne $s.$f
|
| 49 |
-
$icon = if ($exists) { "✅" } else { "❌" }
|
| 50 |
-
Write-Host " $icon Suggestion field '$f' present"
|
| 51 |
-
}
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
Write-Host "`n═══════════════════════════════════════════════════" -ForegroundColor Cyan
|
| 55 |
-
Write-Host "TEST CASE 2: Sequential Apply Input" -ForegroundColor Cyan
|
| 56 |
-
Write-Host "═══════════════════════════════════════════════════" -ForegroundColor Cyan
|
| 57 |
-
|
| 58 |
-
$inputText2 = 'انا ذاهب الي البت'
|
| 59 |
-
Write-Host "`nInput: `"$inputText2`""
|
| 60 |
-
|
| 61 |
-
$body2 = @{ text = $inputText2 } | ConvertTo-Json -Compress
|
| 62 |
-
$response2 = Invoke-RestMethod -Uri 'https://bayan10-bayan-api.hf.space/api/analyze' -Method POST -Body ([System.Text.Encoding]::UTF8.GetBytes($body2)) -ContentType 'application/json; charset=utf-8'
|
| 63 |
-
|
| 64 |
-
Write-Host "`n── API Response ──" -ForegroundColor Yellow
|
| 65 |
-
Write-Host "Status: $($response2.status)"
|
| 66 |
-
Write-Host "Original: `"$($response2.original)`""
|
| 67 |
-
Write-Host "Corrected: `"$($response2.corrected)`""
|
| 68 |
-
Write-Host "Suggestions count: $($response2.suggestions.Count)"
|
| 69 |
-
|
| 70 |
-
Write-Host "`n── Suggestions ──" -ForegroundColor Yellow
|
| 71 |
-
foreach ($s in $response2.suggestions) {
|
| 72 |
-
Write-Host " [$($s.type)] `"$($s.original)`" → `"$($s.correction)`" (start:$($s.start), end:$($s.end))"
|
| 73 |
-
if ($s.alternatives) {
|
| 74 |
-
Write-Host " Alternatives: $($s.alternatives -join ', ')"
|
| 75 |
-
}
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
# Save response for browser test
|
| 79 |
-
$response2 | ConvertTo-Json -Depth 5 | Out-File -FilePath 'e:\Atef''s Shit\extension\tests\api_response_tc2.json' -Encoding UTF8
|
| 80 |
-
Write-Host "`n✅ Response saved to api_response_tc2.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/tests/test_e2e_real.html
DELETED
|
@@ -1,319 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="ar" dir="rtl">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<title>Bayan — E2E Verification</title>
|
| 6 |
-
<style>
|
| 7 |
-
body { font-family: 'Segoe UI', Tahoma, sans-serif; background: #0f0f14; color: #f0f0f5; padding: 24px; max-width: 800px; margin: 0 auto; }
|
| 8 |
-
h1 { color: #818cf8; font-size: 20px; margin-bottom: 20px; }
|
| 9 |
-
h2 { color: #9898ad; font-size: 16px; margin: 20px 0 10px; border-bottom: 1px solid #2d2d3d; padding-bottom: 6px; }
|
| 10 |
-
.test-group { margin-bottom: 16px; padding: 12px; background: #1a1a24; border-radius: 8px; border: 1px solid #2d2d3d; }
|
| 11 |
-
.pass { color: #22c55e; }
|
| 12 |
-
.fail { color: #ef4444; font-weight: bold; }
|
| 13 |
-
.result-line { font-size: 13px; margin: 4px 0; font-family: monospace; direction: ltr; text-align: left; }
|
| 14 |
-
.detail { color: #6b6b80; font-size: 12px; margin-left: 24px; direction: ltr; text-align: left; }
|
| 15 |
-
.summary { margin-top: 24px; padding: 16px; background: #1a1a24; border-radius: 8px; border: 2px solid #818cf8; font-size: 16px; font-weight: bold; direction: ltr; text-align: center; }
|
| 16 |
-
#log { white-space: pre-wrap; font-family: monospace; font-size: 12px; color: #9898ad; background: #111118; padding: 12px; border-radius: 8px; margin-top: 12px; direction: ltr; text-align: left; max-height: 300px; overflow-y: auto; }
|
| 17 |
-
.arabic-text { direction: rtl; text-align: right; font-size: 16px; padding: 8px; background: #22222e; border-radius: 6px; margin: 6px 0; }
|
| 18 |
-
</style>
|
| 19 |
-
</head>
|
| 20 |
-
<body>
|
| 21 |
-
<h1>🔬 Bayan — Real E2E Verification</h1>
|
| 22 |
-
<div id="output"></div>
|
| 23 |
-
<h2>Execution Log</h2>
|
| 24 |
-
<div id="log"></div>
|
| 25 |
-
|
| 26 |
-
<!-- Load extension modules -->
|
| 27 |
-
<script src="../shared/config.js"></script>
|
| 28 |
-
<script src="../shared/bayan-renderer.js"></script>
|
| 29 |
-
<script src="../shared/bayan-ui.js"></script>
|
| 30 |
-
<script src="../shared/bayan-patches.js"></script>
|
| 31 |
-
<script src="../shared/bayan-api.js"></script>
|
| 32 |
-
|
| 33 |
-
<script>
|
| 34 |
-
const outputEl = document.getElementById('output');
|
| 35 |
-
const logEl = document.getElementById('log');
|
| 36 |
-
let passed = 0;
|
| 37 |
-
let failed = 0;
|
| 38 |
-
let currentGroup = null;
|
| 39 |
-
|
| 40 |
-
function log(msg) {
|
| 41 |
-
logEl.textContent += msg + '\n';
|
| 42 |
-
console.log(msg);
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
function group(name) {
|
| 46 |
-
currentGroup = document.createElement('div');
|
| 47 |
-
currentGroup.className = 'test-group';
|
| 48 |
-
currentGroup.innerHTML = `<h2 style="margin:0 0 8px;font-size:14px;color:#818cf8">${name}</h2>`;
|
| 49 |
-
outputEl.appendChild(currentGroup);
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
function assert(condition, message) {
|
| 53 |
-
const el = document.createElement('div');
|
| 54 |
-
el.className = 'result-line';
|
| 55 |
-
if (condition) {
|
| 56 |
-
passed++;
|
| 57 |
-
el.innerHTML = `<span class="pass">✅</span> ${message}`;
|
| 58 |
-
} else {
|
| 59 |
-
failed++;
|
| 60 |
-
el.innerHTML = `<span class="fail">❌</span> ${message}`;
|
| 61 |
-
}
|
| 62 |
-
currentGroup.appendChild(el);
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
function assertEqual(actual, expected, message) {
|
| 66 |
-
if (actual === expected) {
|
| 67 |
-
assert(true, message);
|
| 68 |
-
} else {
|
| 69 |
-
assert(false, message);
|
| 70 |
-
const detail = document.createElement('div');
|
| 71 |
-
detail.className = 'detail';
|
| 72 |
-
detail.textContent = `Expected: "${expected}" | Actual: "${actual}"`;
|
| 73 |
-
currentGroup.appendChild(detail);
|
| 74 |
-
}
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
function showArabic(text) {
|
| 78 |
-
const el = document.createElement('div');
|
| 79 |
-
el.className = 'arabic-text';
|
| 80 |
-
el.textContent = text;
|
| 81 |
-
currentGroup.appendChild(el);
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
// ══════════════════════════════════════════════
|
| 85 |
-
// MAIN TEST RUNNER
|
| 86 |
-
// ══════════════════════════════════════════════
|
| 87 |
-
(async function runTests() {
|
| 88 |
-
|
| 89 |
-
// ═══════════════════════════════════════════
|
| 90 |
-
// TEST CASE 1: Basic Correction Flow
|
| 91 |
-
// ═══════════════════════════════════════════
|
| 92 |
-
group('TEST CASE 1: API Verification');
|
| 93 |
-
const INPUT_1 = 'انا ذهبت الي المدرسه اليوم';
|
| 94 |
-
log(`[TC1] Input: "${INPUT_1}"`);
|
| 95 |
-
|
| 96 |
-
let data1;
|
| 97 |
-
try {
|
| 98 |
-
data1 = await bayanAnalyze(INPUT_1);
|
| 99 |
-
log(`[TC1] API response received: status=${data1.status}, suggestions=${data1.suggestions?.length}`);
|
| 100 |
-
assert(true, 'API call succeeded (no network error)');
|
| 101 |
-
} catch (err) {
|
| 102 |
-
assert(false, `API call failed: ${err.message}`);
|
| 103 |
-
log(`[TC1] ERROR: ${err.message}`);
|
| 104 |
-
return;
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
// Schema validation
|
| 108 |
-
assertEqual(data1.status, 'success', 'Response status = "success"');
|
| 109 |
-
assert(typeof data1.original === 'string', 'Response has "original" (string)');
|
| 110 |
-
assert(typeof data1.corrected === 'string', 'Response has "corrected" (string)');
|
| 111 |
-
assert(Array.isArray(data1.suggestions), 'Response has "suggestions" (array)');
|
| 112 |
-
assert(typeof data1.timing_ms === 'object', 'Response has "timing_ms" (object)');
|
| 113 |
-
assert(data1.suggestions.length > 0, `Suggestions returned: ${data1.suggestions.length}`);
|
| 114 |
-
|
| 115 |
-
// Verify suggestion schema
|
| 116 |
-
if (data1.suggestions.length > 0) {
|
| 117 |
-
const s = data1.suggestions[0];
|
| 118 |
-
assert(typeof s.id === 'string', 'Suggestion has "id"');
|
| 119 |
-
assert(typeof s.start === 'number', 'Suggestion has "start"');
|
| 120 |
-
assert(typeof s.end === 'number', 'Suggestion has "end"');
|
| 121 |
-
assert(typeof s.original === 'string', 'Suggestion has "original"');
|
| 122 |
-
assert(typeof s.correction === 'string', 'Suggestion has "correction"');
|
| 123 |
-
assert(typeof s.type === 'string', 'Suggestion has "type"');
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
// Verify offsets match original text
|
| 127 |
-
group('TEST CASE 1: Offset Correctness');
|
| 128 |
-
const sorted1 = sortSuggestions(data1.suggestions);
|
| 129 |
-
sorted1.forEach((s, i) => {
|
| 130 |
-
const extracted = data1.original.slice(s.start, s.end);
|
| 131 |
-
assertEqual(extracted, s.original, `Suggestion ${i} offset [${s.start}:${s.end}] extracts "${s.original}"`);
|
| 132 |
-
log(`[TC1] Suggestion ${i}: [${s.type}] "${s.original}" → "${s.correction}" offset=[${s.start}:${s.end}]`);
|
| 133 |
-
});
|
| 134 |
-
|
| 135 |
-
// Apply All test
|
| 136 |
-
group('TEST CASE 1: Apply All');
|
| 137 |
-
const correctedText1 = applyAllPatches(data1.original, data1.suggestions);
|
| 138 |
-
showArabic(correctedText1);
|
| 139 |
-
assertEqual(correctedText1, data1.corrected, 'applyAllPatches() === API corrected text');
|
| 140 |
-
log(`[TC1] applyAllPatches result: "${correctedText1}"`);
|
| 141 |
-
log(`[TC1] API corrected field: "${data1.corrected}"`);
|
| 142 |
-
|
| 143 |
-
// Highlighted text test
|
| 144 |
-
group('TEST CASE 1: Rendering');
|
| 145 |
-
const html1 = renderHighlightedText(data1.original, sorted1);
|
| 146 |
-
assert(html1.length > 0, 'renderHighlightedText produces non-empty HTML');
|
| 147 |
-
assert(html1.includes('bayan-spelling-error') || html1.includes('bayan-punctuation-suggestion'), 'HTML contains error highlight classes');
|
| 148 |
-
log(`[TC1] Highlighted HTML length: ${html1.length} chars`);
|
| 149 |
-
|
| 150 |
-
// Score test
|
| 151 |
-
group('TEST CASE 1: Score Calculation');
|
| 152 |
-
const counts1 = countByType(data1.suggestions);
|
| 153 |
-
const score1 = calculateWritingScore(counts1.spelling, counts1.grammar, counts1.punctuation);
|
| 154 |
-
assert(score1 >= 0 && score1 <= 100, `Score in valid range: ${score1}`);
|
| 155 |
-
log(`[TC1] Counts: spelling=${counts1.spelling}, grammar=${counts1.grammar}, punctuation=${counts1.punctuation}`);
|
| 156 |
-
log(`[TC1] Score: ${score1}/100`);
|
| 157 |
-
|
| 158 |
-
// ═══════════════════════════════════════════
|
| 159 |
-
// TEST CASE 2: Sequential Apply
|
| 160 |
-
// ═══════════════════════════════════════════
|
| 161 |
-
group('TEST CASE 2: API Call');
|
| 162 |
-
const INPUT_2 = 'انا ذاهب الي البت';
|
| 163 |
-
log(`\n[TC2] Input: "${INPUT_2}"`);
|
| 164 |
-
|
| 165 |
-
let data2;
|
| 166 |
-
try {
|
| 167 |
-
data2 = await bayanAnalyze(INPUT_2);
|
| 168 |
-
log(`[TC2] API response: status=${data2.status}, suggestions=${data2.suggestions?.length}`);
|
| 169 |
-
assert(true, 'API call succeeded');
|
| 170 |
-
assertEqual(data2.status, 'success', 'Status = success');
|
| 171 |
-
} catch (err) {
|
| 172 |
-
assert(false, `API call failed: ${err.message}`);
|
| 173 |
-
return;
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
// Show what the API actually detected
|
| 177 |
-
group('TEST CASE 2: API-Detected Corrections');
|
| 178 |
-
data2.suggestions.forEach((s, i) => {
|
| 179 |
-
assert(true, `[${s.type}] "${s.original}" → "${s.correction}" (${s.start}:${s.end})`);
|
| 180 |
-
});
|
| 181 |
-
showArabic(`API corrected: ${data2.corrected}`);
|
| 182 |
-
|
| 183 |
-
// Sequential apply using ACTUAL API response
|
| 184 |
-
group('TEST CASE 2: Sequential Apply (Actual API suggestions)');
|
| 185 |
-
let text2 = data2.original;
|
| 186 |
-
let sugs2 = sortSuggestions([...data2.suggestions]);
|
| 187 |
-
log(`[TC2] Starting sequential apply with ${sugs2.length} suggestions`);
|
| 188 |
-
log(`[TC2] Initial text: "${text2}"`);
|
| 189 |
-
|
| 190 |
-
const applyOrder = [...sugs2]; // Apply in sorted order
|
| 191 |
-
for (let i = 0; i < applyOrder.length; i++) {
|
| 192 |
-
const sug = sugs2.find(s => s.id === applyOrder[i].id);
|
| 193 |
-
if (!sug) {
|
| 194 |
-
log(`[TC2] Suggestion ${i} already removed, skipping`);
|
| 195 |
-
continue;
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
// Verify offset before apply
|
| 199 |
-
const beforeExtract = text2.slice(sug.start, sug.end);
|
| 200 |
-
assertEqual(beforeExtract, sug.original, `Step ${i+1}: Offset [${sug.start}:${sug.end}] extracts "${sug.original}" BEFORE apply`);
|
| 201 |
-
|
| 202 |
-
// Apply and rebase
|
| 203 |
-
const result = applyAndRebase(text2, sug, sug.correction, sugs2);
|
| 204 |
-
text2 = result.text;
|
| 205 |
-
sugs2 = result.suggestions;
|
| 206 |
-
|
| 207 |
-
log(`[TC2] Step ${i+1}: Applied "${sug.original}" → "${sug.correction}"`);
|
| 208 |
-
log(`[TC2] Step ${i+1}: Text now: "${text2}"`);
|
| 209 |
-
log(`[TC2] Step ${i+1}: Remaining suggestions: ${sugs2.length}`);
|
| 210 |
-
showArabic(`Step ${i+1}: ${text2}`);
|
| 211 |
-
|
| 212 |
-
// Verify ALL remaining suggestion offsets still extract correct text
|
| 213 |
-
sugs2.forEach((rs, j) => {
|
| 214 |
-
const extracted = text2.slice(rs.start, rs.end);
|
| 215 |
-
assertEqual(extracted, rs.original, `Step ${i+1}: Remaining[${j}] offset [${rs.start}:${rs.end}] extracts "${rs.original}"`);
|
| 216 |
-
});
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
assert(sugs2.length === 0, 'All suggestions applied (0 remaining)');
|
| 220 |
-
assertEqual(text2, data2.corrected, 'Sequential result === API corrected text');
|
| 221 |
-
|
| 222 |
-
// ═══════════════════════════════════════════
|
| 223 |
-
// TEST CASE 2b: Synthetic test with user's exact scenario
|
| 224 |
-
// ═══════════════════════════════════════════
|
| 225 |
-
group('TEST CASE 2b: Synthetic Sequential (User Scenario)');
|
| 226 |
-
// Simulate the exact scenario the user described,
|
| 227 |
-
// using manually-constructed suggestions
|
| 228 |
-
const SYNTH_TEXT = 'انا ذاهب الى البت';
|
| 229 |
-
const synthIdx_ana = SYNTH_TEXT.indexOf('انا');
|
| 230 |
-
const synthIdx_ala = SYNTH_TEXT.indexOf('الى');
|
| 231 |
-
const synthIdx_bet = SYNTH_TEXT.indexOf('البت');
|
| 232 |
-
|
| 233 |
-
log(`[TC2b] Synthetic text: "${SYNTH_TEXT}" (length: ${SYNTH_TEXT.length})`);
|
| 234 |
-
log(`[TC2b] indexOf('انا')=${synthIdx_ana}, indexOf('الى')=${synthIdx_ala}, indexOf('البت')=${synthIdx_bet}`);
|
| 235 |
-
|
| 236 |
-
const synthSugs = [
|
| 237 |
-
{ id: 's1', start: synthIdx_ala, end: synthIdx_ala + 3, original: 'الى', correction: 'إلى', type: 'grammar' },
|
| 238 |
-
{ id: 's2', start: synthIdx_bet, end: synthIdx_bet + 4, original: 'البت', correction: 'البيت', type: 'spelling' },
|
| 239 |
-
{ id: 's3', start: synthIdx_ana, end: synthIdx_ana + 3, original: 'انا', correction: 'أنا', type: 'spelling' },
|
| 240 |
-
];
|
| 241 |
-
|
| 242 |
-
// Verify offsets
|
| 243 |
-
synthSugs.forEach(s => {
|
| 244 |
-
assertEqual(SYNTH_TEXT.slice(s.start, s.end), s.original, `Offset check: [${s.start}:${s.end}] = "${s.original}"`);
|
| 245 |
-
});
|
| 246 |
-
|
| 247 |
-
// Step 1: Apply "الى" → "إلى"
|
| 248 |
-
let sText = SYNTH_TEXT;
|
| 249 |
-
let sSugs = [...synthSugs];
|
| 250 |
-
let r1 = applyAndRebase(sText, sSugs[0], 'إلى', sSugs);
|
| 251 |
-
sText = r1.text;
|
| 252 |
-
sSugs = r1.suggestions;
|
| 253 |
-
showArabic(`Step 1 (الى→إلى): ${sText}`);
|
| 254 |
-
assertEqual(sText, 'انا ذاهب إلى البت', 'Step 1 result correct');
|
| 255 |
-
assert(sSugs.length === 2, 'Step 1: 2 remaining');
|
| 256 |
-
// Verify remaining offsets
|
| 257 |
-
sSugs.forEach(s => {
|
| 258 |
-
assertEqual(sText.slice(s.start, s.end), s.original, `Step 1: Remaining "${s.original}" offset valid`);
|
| 259 |
-
});
|
| 260 |
-
|
| 261 |
-
// Step 2: Apply "البت" → "البيت" (should be at shifted offset)
|
| 262 |
-
const sugBet = sSugs.find(s => s.original === 'البت');
|
| 263 |
-
assert(!!sugBet, 'Found "البت" suggestion in remaining');
|
| 264 |
-
let r2 = applyAndRebase(sText, sugBet, 'البيت', sSugs);
|
| 265 |
-
sText = r2.text;
|
| 266 |
-
sSugs = r2.suggestions;
|
| 267 |
-
showArabic(`Step 2 (البت→البيت): ${sText}`);
|
| 268 |
-
assertEqual(sText, 'انا ذاهب إلى البيت', 'Step 2 result correct');
|
| 269 |
-
assert(sSugs.length === 1, 'Step 2: 1 remaining');
|
| 270 |
-
sSugs.forEach(s => {
|
| 271 |
-
assertEqual(sText.slice(s.start, s.end), s.original, `Step 2: Remaining "${s.original}" offset valid`);
|
| 272 |
-
});
|
| 273 |
-
|
| 274 |
-
// Step 3: Apply "انا" → "أنا"
|
| 275 |
-
const sugAna = sSugs.find(s => s.original === 'انا');
|
| 276 |
-
assert(!!sugAna, 'Found "انا" suggestion in remaining');
|
| 277 |
-
let r3 = applyAndRebase(sText, sugAna, 'أنا', sSugs);
|
| 278 |
-
sText = r3.text;
|
| 279 |
-
sSugs = r3.suggestions;
|
| 280 |
-
showArabic(`Step 3 (انا→أنا): ${sText}`);
|
| 281 |
-
|
| 282 |
-
// FINAL VERIFICATION
|
| 283 |
-
assertEqual(sText, 'أنا ذاهب إلى البيت', '🏁 FINAL RESULT matches expected');
|
| 284 |
-
assert(sSugs.length === 0, 'All suggestions consumed');
|
| 285 |
-
|
| 286 |
-
// Verify sequential === applyAllPatches
|
| 287 |
-
const bulkResult = applyAllPatches(SYNTH_TEXT, synthSugs);
|
| 288 |
-
assertEqual(sText, bulkResult, 'Sequential === applyAllPatches');
|
| 289 |
-
|
| 290 |
-
// ═══════════════════════════════════════════
|
| 291 |
-
// SUMMARY
|
| 292 |
-
// ═══════════════════════════════════════════
|
| 293 |
-
const summary = document.createElement('div');
|
| 294 |
-
summary.className = 'summary';
|
| 295 |
-
summary.style.color = failed > 0 ? '#ef4444' : '#22c55e';
|
| 296 |
-
summary.textContent = `Results: ${passed} passed, ${failed} failed`;
|
| 297 |
-
outputEl.appendChild(summary);
|
| 298 |
-
|
| 299 |
-
const verdict = document.createElement('div');
|
| 300 |
-
verdict.className = 'summary';
|
| 301 |
-
verdict.style.marginTop = '12px';
|
| 302 |
-
if (failed === 0) {
|
| 303 |
-
verdict.style.color = '#22c55e';
|
| 304 |
-
verdict.style.borderColor = '#22c55e';
|
| 305 |
-
verdict.textContent = 'HIGH-1 = COMPLETE (REAL VERIFIED) ✅';
|
| 306 |
-
} else {
|
| 307 |
-
verdict.style.color = '#ef4444';
|
| 308 |
-
verdict.style.borderColor = '#ef4444';
|
| 309 |
-
verdict.textContent = `HIGH-1 = INCOMPLETE (${failed} failures)`;
|
| 310 |
-
}
|
| 311 |
-
outputEl.appendChild(verdict);
|
| 312 |
-
|
| 313 |
-
log(`\n${'═'.repeat(50)}`);
|
| 314 |
-
log(`Results: ${passed} passed, ${failed} failed`);
|
| 315 |
-
log(`${'═'.repeat(50)}`);
|
| 316 |
-
})();
|
| 317 |
-
</script>
|
| 318 |
-
</body>
|
| 319 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/tests/test_inline.html
DELETED
|
@@ -1,78 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="ar" dir="rtl">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<title>Bayan Inline Engine — Test</title>
|
| 6 |
-
<style>
|
| 7 |
-
body { font-family: 'Segoe UI', Tahoma, sans-serif; background: #1a1a2e; color: #f0f0f5; padding: 40px; }
|
| 8 |
-
h1 { color: #818cf8; margin-bottom: 20px; }
|
| 9 |
-
textarea { width: 100%; height: 150px; font-size: 16px; padding: 12px; border-radius: 8px;
|
| 10 |
-
border: 1px solid #3a3a4d; background: #22222e; color: #f0f0f5; direction: rtl; }
|
| 11 |
-
.field-group { margin-bottom: 30px; }
|
| 12 |
-
label { display: block; margin-bottom: 8px; color: #9898ad; }
|
| 13 |
-
[contenteditable] { width: 100%; min-height: 100px; font-size: 16px; padding: 12px; border-radius: 8px;
|
| 14 |
-
border: 1px solid #3a3a4d; background: #22222e; color: #f0f0f5; direction: rtl; outline: none; }
|
| 15 |
-
[contenteditable]:focus { border-color: #6366f1; }
|
| 16 |
-
.instructions { background: #22222e; padding: 16px; border-radius: 8px; margin-bottom: 30px; font-size: 13px; color: #9898ad; }
|
| 17 |
-
.instructions code { background: #2a2a38; padding: 2px 6px; border-radius: 4px; color: #818cf8; }
|
| 18 |
-
</style>
|
| 19 |
-
</head>
|
| 20 |
-
<body>
|
| 21 |
-
<h1>🧪 اختبار محرك بيان المضمّن (Phase 6)</h1>
|
| 22 |
-
|
| 23 |
-
<div class="instructions">
|
| 24 |
-
<p><strong>كيفية الاختبار:</strong></p>
|
| 25 |
-
<ol>
|
| 26 |
-
<li>اكتب أو الصق نصاً عربياً يحتوي على أخطاء في أي حقل أدناه</li>
|
| 27 |
-
<li>انتظر ثانية واحدة — ستظهر أيقونة بيان الصغيرة</li>
|
| 28 |
-
<li>ستظهر الأخطاء بخطوط حمراء/برتقالية/زرقاء</li>
|
| 29 |
-
<li>انقر على كلمة مُعلّمة لرؤية الاقتراح</li>
|
| 30 |
-
</ol>
|
| 31 |
-
<p>نص اختباري: <code>انا ذهبت الي المدرسه اليوم وكان الجو جميل جدا</code></p>
|
| 32 |
-
</div>
|
| 33 |
-
|
| 34 |
-
<div class="field-group">
|
| 35 |
-
<label>📝 حقل نصي (textarea)</label>
|
| 36 |
-
<textarea id="test-textarea" placeholder="اكتب نصاً عربياً هنا..."></textarea>
|
| 37 |
-
</div>
|
| 38 |
-
|
| 39 |
-
<div class="field-group">
|
| 40 |
-
<label>✏️ حقل قابل للتحرير (contenteditable)</label>
|
| 41 |
-
<div contenteditable="true" id="test-contenteditable"></div>
|
| 42 |
-
</div>
|
| 43 |
-
|
| 44 |
-
<div class="field-group">
|
| 45 |
-
<label>🔤 حقل إدخال (input)</label>
|
| 46 |
-
<input type="text" id="test-input" style="width:100%;font-size:16px;padding:10px;border-radius:8px;border:1px solid #3a3a4d;background:#22222e;color:#f0f0f5;direction:rtl;" placeholder="اكتب هنا...">
|
| 47 |
-
</div>
|
| 48 |
-
|
| 49 |
-
<div id="console-log" style="margin-top: 30px; padding: 12px; background: #0f0f14; border-radius: 8px; font-family: monospace; font-size: 11px; color: #6b6b80; max-height: 200px; overflow-y: auto;">
|
| 50 |
-
<div>Bayan Inline Test Page Ready</div>
|
| 51 |
-
</div>
|
| 52 |
-
|
| 53 |
-
<script>
|
| 54 |
-
// Capture console.log/warn to show on page
|
| 55 |
-
const logDiv = document.getElementById('console-log');
|
| 56 |
-
const origLog = console.log;
|
| 57 |
-
const origWarn = console.warn;
|
| 58 |
-
console.log = (...args) => {
|
| 59 |
-
origLog(...args);
|
| 60 |
-
if (args[0]?.includes?.('[Bayan')) {
|
| 61 |
-
const el = document.createElement('div');
|
| 62 |
-
el.textContent = args.join(' ');
|
| 63 |
-
el.style.color = '#22c55e';
|
| 64 |
-
logDiv.appendChild(el);
|
| 65 |
-
logDiv.scrollTop = logDiv.scrollHeight;
|
| 66 |
-
}
|
| 67 |
-
};
|
| 68 |
-
console.warn = (...args) => {
|
| 69 |
-
origWarn(...args);
|
| 70 |
-
const el = document.createElement('div');
|
| 71 |
-
el.textContent = args.join(' ');
|
| 72 |
-
el.style.color = '#f59e0b';
|
| 73 |
-
logDiv.appendChild(el);
|
| 74 |
-
logDiv.scrollTop = logDiv.scrollHeight;
|
| 75 |
-
};
|
| 76 |
-
</script>
|
| 77 |
-
</body>
|
| 78 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/tests/test_patches.html
DELETED
|
@@ -1,282 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<title>Bayan Patch Engine — Tests</title>
|
| 6 |
-
<style>
|
| 7 |
-
body { font-family: 'Segoe UI', Tahoma, sans-serif; background: #0f0f14; color: #f0f0f5; padding: 24px; }
|
| 8 |
-
h1 { color: #818cf8; font-size: 20px; margin-bottom: 20px; }
|
| 9 |
-
.test-group { margin-bottom: 16px; padding: 12px; background: #1a1a24; border-radius: 8px; border: 1px solid #2d2d3d; }
|
| 10 |
-
.test-group h2 { font-size: 14px; color: #9898ad; margin-bottom: 8px; }
|
| 11 |
-
.pass { color: #22c55e; }
|
| 12 |
-
.fail { color: #ef4444; font-weight: bold; }
|
| 13 |
-
.result-line { font-size: 13px; margin: 4px 0; font-family: monospace; }
|
| 14 |
-
.detail { color: #6b6b80; font-size: 12px; margin-left: 24px; }
|
| 15 |
-
.summary { margin-top: 24px; padding: 16px; background: #1a1a24; border-radius: 8px; border: 2px solid #818cf8; font-size: 16px; font-weight: bold; }
|
| 16 |
-
</style>
|
| 17 |
-
</head>
|
| 18 |
-
<body>
|
| 19 |
-
<h1>🧪 Bayan Patch Engine — Test Suite</h1>
|
| 20 |
-
<div id="output"></div>
|
| 21 |
-
|
| 22 |
-
<!-- Load modules in dependency order -->
|
| 23 |
-
<script src="../shared/config.js"></script>
|
| 24 |
-
<script src="../shared/bayan-renderer.js"></script>
|
| 25 |
-
<script src="../shared/bayan-ui.js"></script>
|
| 26 |
-
<script src="../shared/bayan-patches.js"></script>
|
| 27 |
-
|
| 28 |
-
<script>
|
| 29 |
-
const output = document.getElementById('output');
|
| 30 |
-
let passed = 0;
|
| 31 |
-
let failed = 0;
|
| 32 |
-
let currentGroup = null;
|
| 33 |
-
|
| 34 |
-
function group(name) {
|
| 35 |
-
currentGroup = document.createElement('div');
|
| 36 |
-
currentGroup.className = 'test-group';
|
| 37 |
-
currentGroup.innerHTML = `<h2>${name}</h2>`;
|
| 38 |
-
output.appendChild(currentGroup);
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
function assert(condition, message) {
|
| 42 |
-
const el = document.createElement('div');
|
| 43 |
-
el.className = 'result-line';
|
| 44 |
-
if (condition) {
|
| 45 |
-
passed++;
|
| 46 |
-
el.innerHTML = `<span class="pass">✅</span> ${message}`;
|
| 47 |
-
} else {
|
| 48 |
-
failed++;
|
| 49 |
-
el.innerHTML = `<span class="fail">❌</span> ${message}`;
|
| 50 |
-
}
|
| 51 |
-
currentGroup.appendChild(el);
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
function assertEqual(actual, expected, message) {
|
| 55 |
-
if (actual === expected) {
|
| 56 |
-
assert(true, message);
|
| 57 |
-
} else {
|
| 58 |
-
assert(false, message);
|
| 59 |
-
const detail = document.createElement('div');
|
| 60 |
-
detail.className = 'detail';
|
| 61 |
-
detail.textContent = `Expected: "${expected}" | Actual: "${actual}"`;
|
| 62 |
-
currentGroup.appendChild(detail);
|
| 63 |
-
}
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
// ══════════════════════════════════════════════════════
|
| 67 |
-
// Test data — offsets verified via JavaScript indexOf()
|
| 68 |
-
// ══════════════════════════════════════════════════════
|
| 69 |
-
//
|
| 70 |
-
// TEXT = 'أنا ذاهب الى البت والمدرسه' (length: 26)
|
| 71 |
-
//
|
| 72 |
-
// indexOf('الى') = 9 → slice(9, 12) = 'الى' (3 chars)
|
| 73 |
-
// indexOf('البت') = 13 → slice(13, 17) = 'البت' (4 chars)
|
| 74 |
-
// indexOf('المدرسه') = 19 → slice(19, 26) = 'المدرسه' (7 chars)
|
| 75 |
-
//
|
| 76 |
-
|
| 77 |
-
const TEXT = 'أنا ذاهب الى البت والمدرسه';
|
| 78 |
-
|
| 79 |
-
function makeSuggestions() {
|
| 80 |
-
return [
|
| 81 |
-
{ id: 'a', start: 9, end: 12, original: 'الى', correction: 'إلى', type: 'grammar' },
|
| 82 |
-
{ id: 'b', start: 13, end: 17, original: 'البت', correction: 'البيت', type: 'spelling' },
|
| 83 |
-
{ id: 'c', start: 19, end: 26, original: 'المدرسه', correction: 'المدرسة', type: 'spelling' },
|
| 84 |
-
];
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
// ── Prerequisite: verify test data offsets ──
|
| 88 |
-
group('PREREQ: Verify test data offsets');
|
| 89 |
-
assertEqual(TEXT.length, 26, 'Text length = 26');
|
| 90 |
-
assertEqual(TEXT.slice(9, 12), 'الى', 'slice(9,12) = "الى"');
|
| 91 |
-
assertEqual(TEXT.slice(13, 17), 'البت', 'slice(13,17) = "البت"');
|
| 92 |
-
assertEqual(TEXT.slice(19, 26), 'المدرسه', 'slice(19,26) = "المدرسه"');
|
| 93 |
-
|
| 94 |
-
// ══════════════════════════════════════════════════════
|
| 95 |
-
// TEST 1: Same-length replacement (delta = 0)
|
| 96 |
-
// "الى" (3) → "إلى" (3), delta = 0
|
| 97 |
-
// ══════════════════════════════════════════════════════
|
| 98 |
-
group('TEST 1: Same-length replacement (delta = 0)');
|
| 99 |
-
{
|
| 100 |
-
const suggestions = makeSuggestions();
|
| 101 |
-
const result = applyAndRebase(TEXT, suggestions[0], 'إلى', suggestions);
|
| 102 |
-
|
| 103 |
-
assertEqual(result.text, 'أنا ذاهب إلى البت والمدرسه', 'Text: "الى" → "إلى"');
|
| 104 |
-
assert(result.suggestions.length === 2, 'Removed applied (2 remaining)');
|
| 105 |
-
assertEqual(result.suggestions[0].start, 13, 'B.start unchanged (13)');
|
| 106 |
-
assertEqual(result.suggestions[0].end, 17, 'B.end unchanged (17)');
|
| 107 |
-
assertEqual(result.suggestions[1].start, 19, 'C.start unchanged (19)');
|
| 108 |
-
assertEqual(result.suggestions[1].end, 26, 'C.end unchanged (26)');
|
| 109 |
-
// Verify offsets extract correct text from modified string
|
| 110 |
-
assertEqual(result.text.slice(result.suggestions[0].start, result.suggestions[0].end), 'البت', 'Rebased B extracts "البت"');
|
| 111 |
-
assertEqual(result.text.slice(result.suggestions[1].start, result.suggestions[1].end), 'المدرسه', 'Rebased C extracts "المدرسه"');
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
// ══════════════════════════════════════════════════════
|
| 115 |
-
// TEST 2: Longer replacement (delta > 0)
|
| 116 |
-
// "البت" (4) → "البيت" (5), delta = +1
|
| 117 |
-
// ══════════════════════════════════════════════════════
|
| 118 |
-
group('TEST 2: Longer replacement (delta > 0)');
|
| 119 |
-
{
|
| 120 |
-
const suggestions = makeSuggestions();
|
| 121 |
-
const result = applyAndRebase(TEXT, suggestions[1], 'البيت', suggestions);
|
| 122 |
-
|
| 123 |
-
assertEqual(result.text, 'أنا ذاهب الى البيت والمدرسه', 'Text: "البت" → "البيت"');
|
| 124 |
-
assert(result.suggestions.length === 2, 'Removed applied (2 remaining)');
|
| 125 |
-
|
| 126 |
-
// A is BEFORE B — no shift
|
| 127 |
-
assertEqual(result.suggestions[0].start, 9, 'A.start unchanged (9)');
|
| 128 |
-
assertEqual(result.suggestions[0].end, 12, 'A.end unchanged (12)');
|
| 129 |
-
|
| 130 |
-
// C is AFTER B — shift by +1
|
| 131 |
-
assertEqual(result.suggestions[1].start, 20, 'C.start shifted 19→20 (+1)');
|
| 132 |
-
assertEqual(result.suggestions[1].end, 27, 'C.end shifted 26→27 (+1)');
|
| 133 |
-
|
| 134 |
-
// Verify rebased offsets
|
| 135 |
-
assertEqual(result.text.slice(result.suggestions[0].start, result.suggestions[0].end), 'الى', 'Rebased A extracts "الى"');
|
| 136 |
-
assertEqual(result.text.slice(result.suggestions[1].start, result.suggestions[1].end), 'المدرسه', 'Rebased C extracts "المدرسه"');
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
// ══════════════════════════════════════════════════════
|
| 140 |
-
// TEST 3: Shorter replacement (delta < 0)
|
| 141 |
-
// ══════════════════════════════════════════════════════
|
| 142 |
-
group('TEST 3: Shorter replacement (delta < 0)');
|
| 143 |
-
{
|
| 144 |
-
const text = 'هذا النصص خاطئ والكلمه';
|
| 145 |
-
// Verify offsets
|
| 146 |
-
const idx1 = text.indexOf('النصص');
|
| 147 |
-
const idx2 = text.indexOf('والكلمه');
|
| 148 |
-
const suggestions = [
|
| 149 |
-
{ id: 'x', start: idx1, end: idx1 + 5, original: 'النصص', correction: 'النص', type: 'spelling' },
|
| 150 |
-
{ id: 'y', start: idx2, end: idx2 + 7, original: 'والكلمه', correction: 'والكلمة', type: 'spelling' },
|
| 151 |
-
];
|
| 152 |
-
|
| 153 |
-
// "النصص" (5) → "النص" (4), delta = -1
|
| 154 |
-
const result = applyAndRebase(text, suggestions[0], 'النص', suggestions);
|
| 155 |
-
|
| 156 |
-
assertEqual(result.text, 'هذا النص خاطئ والكلمه', 'Text: "النصص" → "النص"');
|
| 157 |
-
assert(result.suggestions.length === 1, '1 remaining');
|
| 158 |
-
assertEqual(result.suggestions[0].start, idx2 - 1, 'Y.start shifted by -1');
|
| 159 |
-
assertEqual(result.suggestions[0].end, idx2 + 7 - 1, 'Y.end shifted by -1');
|
| 160 |
-
assertEqual(
|
| 161 |
-
result.text.slice(result.suggestions[0].start, result.suggestions[0].end),
|
| 162 |
-
'والكلمه', 'Rebased Y extracts "والكلمه"'
|
| 163 |
-
);
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
// ══════════════════════════════════════════════════════
|
| 167 |
-
// TEST 4: Multiple sequential applies
|
| 168 |
-
// ══════════════════════════════════════════════════════
|
| 169 |
-
group('TEST 4: Multiple sequential applies');
|
| 170 |
-
{
|
| 171 |
-
let text = TEXT;
|
| 172 |
-
let suggestions = makeSuggestions();
|
| 173 |
-
|
| 174 |
-
// Apply A first (same-length, delta=0)
|
| 175 |
-
let r1 = applyAndRebase(text, suggestions[0], 'إلى', suggestions);
|
| 176 |
-
text = r1.text;
|
| 177 |
-
suggestions = r1.suggestions;
|
| 178 |
-
assertEqual(text, 'أنا ذاهب إلى البت والمدرسه', 'After apply A');
|
| 179 |
-
assert(suggestions.length === 2, '2 remaining after A');
|
| 180 |
-
|
| 181 |
-
// Apply B (longer, delta=+1)
|
| 182 |
-
let r2 = applyAndRebase(text, suggestions[0], 'البيت', suggestions);
|
| 183 |
-
text = r2.text;
|
| 184 |
-
suggestions = r2.suggestions;
|
| 185 |
-
assertEqual(text, 'أنا ذاهب إلى البيت والمدرسه', 'After apply B');
|
| 186 |
-
assert(suggestions.length === 1, '1 remaining after B');
|
| 187 |
-
|
| 188 |
-
// Apply C (same-length, delta=0)
|
| 189 |
-
let r3 = applyAndRebase(text, suggestions[0], 'المدرسة', suggestions);
|
| 190 |
-
text = r3.text;
|
| 191 |
-
suggestions = r3.suggestions;
|
| 192 |
-
assertEqual(text, 'أنا ذاهب إلى البيت والمدرسة', 'After apply C — fully corrected');
|
| 193 |
-
assert(suggestions.length === 0, '0 remaining — all applied');
|
| 194 |
-
|
| 195 |
-
// Verify equals applyAllPatches
|
| 196 |
-
const expectedFull = applyAllPatches(TEXT, makeSuggestions());
|
| 197 |
-
assertEqual(text, expectedFull, 'Sequential === applyAllPatches');
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
// ══════════════════════════════════════════════════════
|
| 201 |
-
// TEST 5: Apply last suggestion (no rebase needed)
|
| 202 |
-
// ══════════════════════════════════════════════════════
|
| 203 |
-
group('TEST 5: Apply last suggestion');
|
| 204 |
-
{
|
| 205 |
-
const suggestions = makeSuggestions();
|
| 206 |
-
const result = applyAndRebase(TEXT, suggestions[2], 'المدرسة', suggestions);
|
| 207 |
-
|
| 208 |
-
assertEqual(result.text, 'أنا ذاهب الى البت والمدرسة', 'Last suggestion applied');
|
| 209 |
-
assert(result.suggestions.length === 2, '2 remaining');
|
| 210 |
-
assertEqual(result.suggestions[0].start, 9, 'A unchanged');
|
| 211 |
-
assertEqual(result.suggestions[1].start, 13, 'B unchanged');
|
| 212 |
-
// Verify offsets still work
|
| 213 |
-
assertEqual(result.text.slice(result.suggestions[0].start, result.suggestions[0].end), 'الى', 'A extracts correctly');
|
| 214 |
-
assertEqual(result.text.slice(result.suggestions[1].start, result.suggestions[1].end), 'البت', 'B extracts correctly');
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
// ══════════════════════════════════════════════════════
|
| 218 |
-
// TEST 6: Dismiss (removeSuggestion — no rebase)
|
| 219 |
-
// ══════════════════════════════════════════════════════
|
| 220 |
-
group('TEST 6: Dismiss (removeSuggestion)');
|
| 221 |
-
{
|
| 222 |
-
const suggestions = makeSuggestions();
|
| 223 |
-
const remaining = removeSuggestion(suggestions, 'b');
|
| 224 |
-
assert(remaining.length === 2, '2 remaining after dismiss');
|
| 225 |
-
assert(remaining.every(s => s.id !== 'b'), 'Dismissed suggestion removed');
|
| 226 |
-
assertEqual(remaining[0].start, 9, 'A offset unchanged');
|
| 227 |
-
assertEqual(remaining[1].start, 19, 'C offset unchanged');
|
| 228 |
-
}
|
| 229 |
-
|
| 230 |
-
// ══════════════════════════════════════════════════════
|
| 231 |
-
// TEST 7: applyAllPatches (reverse-order)
|
| 232 |
-
// ══════════════════════════════════════════════════════
|
| 233 |
-
group('TEST 7: applyAllPatches');
|
| 234 |
-
{
|
| 235 |
-
const result = applyAllPatches(TEXT, makeSuggestions());
|
| 236 |
-
assertEqual(result, 'أنا ذاهب إلى البيت والمدرسة', 'All patches applied correctly');
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
-
// ══════════════════════════════════════════════════════
|
| 240 |
-
// TEST 8: countByType
|
| 241 |
-
// ══════════════════════════════════════════════════════
|
| 242 |
-
group('TEST 8: countByType');
|
| 243 |
-
{
|
| 244 |
-
const counts = countByType(makeSuggestions());
|
| 245 |
-
assertEqual(counts.spelling, 2, '2 spelling');
|
| 246 |
-
assertEqual(counts.grammar, 1, '1 grammar');
|
| 247 |
-
assertEqual(counts.punctuation, 0, '0 punctuation');
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
// ══════════════════════════════════════════════════════
|
| 251 |
-
// TEST 9: Apply first suggestion at start=0
|
| 252 |
-
// ══════════════════════════════════════════════════════
|
| 253 |
-
group('TEST 9: Apply first suggestion (start=0)');
|
| 254 |
-
{
|
| 255 |
-
const text = 'الخطاء في البداية';
|
| 256 |
-
const i1 = text.indexOf('الخطاء');
|
| 257 |
-
const suggestions = [
|
| 258 |
-
{ id: 'f', start: i1, end: i1 + 6, original: 'الخطاء', correction: 'الخطأ', type: 'spelling' },
|
| 259 |
-
{ id: 'g', start: 10, end: 17, original: 'البداية', correction: 'البدايه', type: 'grammar' },
|
| 260 |
-
];
|
| 261 |
-
|
| 262 |
-
// "الخطاء" (6) → "الخطأ" (5), delta = -1
|
| 263 |
-
const result = applyAndRebase(text, suggestions[0], 'الخطأ', suggestions);
|
| 264 |
-
assertEqual(result.text.slice(0, 5), 'الخطأ', 'First word replaced');
|
| 265 |
-
assertEqual(result.suggestions[0].start, 9, 'G.start shifted 10→9 (-1)');
|
| 266 |
-
assertEqual(result.suggestions[0].end, 16, 'G.end shifted 17→16 (-1)');
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
// ══════════════════════════════════════════════════════
|
| 270 |
-
// Summary
|
| 271 |
-
// ═══════════════════════���══════════════════════════════
|
| 272 |
-
const summary = document.createElement('div');
|
| 273 |
-
summary.className = 'summary';
|
| 274 |
-
summary.style.color = failed > 0 ? '#ef4444' : '#22c55e';
|
| 275 |
-
summary.textContent = `Results: ${passed} passed, ${failed} failed`;
|
| 276 |
-
output.appendChild(summary);
|
| 277 |
-
|
| 278 |
-
// Also log to console for easy capture
|
| 279 |
-
console.log(`[TEST RESULTS] ${passed} passed, ${failed} failed`);
|
| 280 |
-
</script>
|
| 281 |
-
</body>
|
| 282 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/tests/test_patches.js
DELETED
|
@@ -1,253 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Bayan Chrome Extension — Patch Engine Tests
|
| 3 |
-
*
|
| 4 |
-
* Tests for HIGH-1 fix: applyAndRebase()
|
| 5 |
-
* Run with: node extension/tests/test_patches.js
|
| 6 |
-
*
|
| 7 |
-
* Test scenarios:
|
| 8 |
-
* 1. Shorter replacement (delta < 0)
|
| 9 |
-
* 2. Longer replacement (delta > 0)
|
| 10 |
-
* 3. Same-length replacement (delta = 0)
|
| 11 |
-
* 4. Multiple sequential applies
|
| 12 |
-
* 5. Apply first suggestion (edge case)
|
| 13 |
-
* 6. Apply last suggestion (edge case)
|
| 14 |
-
* 7. Dismiss (no rebase needed)
|
| 15 |
-
* 8. Apply all patches
|
| 16 |
-
*/
|
| 17 |
-
|
| 18 |
-
// ── Load modules (Node.js — no DOM required) ──
|
| 19 |
-
// These are global-scope scripts, so we eval them
|
| 20 |
-
const fs = require('fs');
|
| 21 |
-
const path = require('path');
|
| 22 |
-
|
| 23 |
-
function loadModule(filename) {
|
| 24 |
-
const code = fs.readFileSync(path.join(__dirname, '..', 'shared', filename), 'utf-8');
|
| 25 |
-
eval(code);
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
// Load in dependency order
|
| 29 |
-
loadModule('config.js');
|
| 30 |
-
loadModule('bayan-renderer.js');
|
| 31 |
-
loadModule('bayan-ui.js');
|
| 32 |
-
loadModule('bayan-patches.js');
|
| 33 |
-
|
| 34 |
-
// ── Test utilities ──
|
| 35 |
-
let passed = 0;
|
| 36 |
-
let failed = 0;
|
| 37 |
-
|
| 38 |
-
function assert(condition, message) {
|
| 39 |
-
if (condition) {
|
| 40 |
-
passed++;
|
| 41 |
-
console.log(` ✅ ${message}`);
|
| 42 |
-
} else {
|
| 43 |
-
failed++;
|
| 44 |
-
console.error(` ❌ ${message}`);
|
| 45 |
-
}
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
function assertEqual(actual, expected, message) {
|
| 49 |
-
if (actual === expected) {
|
| 50 |
-
passed++;
|
| 51 |
-
console.log(` ✅ ${message}`);
|
| 52 |
-
} else {
|
| 53 |
-
failed++;
|
| 54 |
-
console.error(` ❌ ${message}`);
|
| 55 |
-
console.error(` Expected: "${expected}"`);
|
| 56 |
-
console.error(` Actual: "${actual}"`);
|
| 57 |
-
}
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
// ══════════════════════════════════════════════════════════
|
| 61 |
-
// Test data: Arabic text with 3 suggestions
|
| 62 |
-
// ══════════════════════════════════════════════════════════
|
| 63 |
-
//
|
| 64 |
-
// Text: "أنا ذاهب الى البت والمدرسه"
|
| 65 |
-
// 0123456789012345678901234567
|
| 66 |
-
//
|
| 67 |
-
// Suggestion A: "الى" → "إلى" (start:10, end:13) — same length (3→3, delta=0)
|
| 68 |
-
// Suggestion B: "البت" → "البيت" (start:14, end:17) — longer (+1, delta=+1)
|
| 69 |
-
// Suggestion C: "المدرسه" → "المدرسة" (start:19, end:26) — same length (7→7, delta=0)
|
| 70 |
-
//
|
| 71 |
-
|
| 72 |
-
const TEXT = 'أنا ذاهب الى البت والمدرسه';
|
| 73 |
-
|
| 74 |
-
function makeSuggestions() {
|
| 75 |
-
return [
|
| 76 |
-
{ id: 'a', start: 10, end: 13, original: 'الى', correction: 'إلى', type: 'grammar' },
|
| 77 |
-
{ id: 'b', start: 14, end: 17, original: 'البت', correction: 'البيت', type: 'spelling' },
|
| 78 |
-
{ id: 'c', start: 19, end: 26, original: 'المدرسه', correction: 'المدرسة', type: 'spelling' },
|
| 79 |
-
];
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
// ══════════════════════════════════════════════════════════
|
| 83 |
-
// TEST 1: Same-length replacement (delta = 0)
|
| 84 |
-
// ══════════════════════════════════════════════════════════
|
| 85 |
-
console.log('\n── TEST 1: Same-length replacement (delta = 0) ──');
|
| 86 |
-
{
|
| 87 |
-
const suggestions = makeSuggestions();
|
| 88 |
-
const result = applyAndRebase(TEXT, suggestions[0], 'إلى', suggestions);
|
| 89 |
-
|
| 90 |
-
assertEqual(result.text, 'أنا ذاهب إلى البت والمدرسه', 'Text: "الى" → "إلى"');
|
| 91 |
-
assert(result.suggestions.length === 2, 'Removed applied suggestion (2 remaining)');
|
| 92 |
-
assertEqual(result.suggestions[0].start, 14, 'Suggestion B start unchanged (14)');
|
| 93 |
-
assertEqual(result.suggestions[0].end, 17, 'Suggestion B end unchanged (17)');
|
| 94 |
-
assertEqual(result.suggestions[1].start, 19, 'Suggestion C start unchanged (19)');
|
| 95 |
-
assertEqual(result.suggestions[1].end, 26, 'Suggestion C end unchanged (26)');
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
// ══════════════════════════════════════════════════════════
|
| 99 |
-
// TEST 2: Longer replacement (delta > 0)
|
| 100 |
-
// ══════════════════════════════════════════════════════════
|
| 101 |
-
console.log('\n── TEST 2: Longer replacement (delta > 0) ──');
|
| 102 |
-
{
|
| 103 |
-
const suggestions = makeSuggestions();
|
| 104 |
-
// Apply B: "البت" (3 chars) → "البيت" (5 chars), delta = +2
|
| 105 |
-
const result = applyAndRebase(TEXT, suggestions[1], 'البيت', suggestions);
|
| 106 |
-
|
| 107 |
-
assertEqual(result.text, 'أنا ذاهب الى البيت والمدرسه', 'Text: "البت" → "البيت"');
|
| 108 |
-
assert(result.suggestions.length === 2, 'Removed applied suggestion (2 remaining)');
|
| 109 |
-
|
| 110 |
-
// A is BEFORE B — should NOT shift
|
| 111 |
-
assertEqual(result.suggestions[0].start, 10, 'Suggestion A start unchanged (10)');
|
| 112 |
-
assertEqual(result.suggestions[0].end, 13, 'Suggestion A end unchanged (13)');
|
| 113 |
-
|
| 114 |
-
// C is AFTER B — should shift by +2
|
| 115 |
-
assertEqual(result.suggestions[1].start, 21, 'Suggestion C start shifted 19→21 (+2)');
|
| 116 |
-
assertEqual(result.suggestions[1].end, 28, 'Suggestion C end shifted 26→28 (+2)');
|
| 117 |
-
|
| 118 |
-
// Verify the shifted offsets are correct
|
| 119 |
-
assertEqual(result.text.substring(result.suggestions[1].start, result.suggestions[1].end),
|
| 120 |
-
'المدرسه', 'Rebased offset C extracts correct text');
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
// ══════════════════════════════════════════════════════════
|
| 124 |
-
// TEST 3: Shorter replacement (delta < 0)
|
| 125 |
-
// ══════════════════════════════════════════════════════════
|
| 126 |
-
console.log('\n── TEST 3: Shorter replacement (delta < 0) ──');
|
| 127 |
-
{
|
| 128 |
-
// Custom text for this test
|
| 129 |
-
const text = 'هذا النصص خاطئ والكلمه خطأ';
|
| 130 |
-
// "النصص" (5) → "النص" (4), delta = -1
|
| 131 |
-
const suggestions = [
|
| 132 |
-
{ id: 'x', start: 4, end: 9, original: 'النصص', correction: 'النص', type: 'spelling' },
|
| 133 |
-
{ id: 'y', start: 17, end: 23, original: 'والكلمه', correction: 'والكلمة', type: 'spelling' },
|
| 134 |
-
];
|
| 135 |
-
|
| 136 |
-
const result = applyAndRebase(text, suggestions[0], 'النص', suggestions);
|
| 137 |
-
|
| 138 |
-
assertEqual(result.text, 'هذا النص خاطئ والكلمه خطأ', 'Text: "النصص" → "النص"');
|
| 139 |
-
assert(result.suggestions.length === 1, '1 remaining');
|
| 140 |
-
assertEqual(result.suggestions[0].start, 16, 'Suggestion Y start shifted 17→16 (-1)');
|
| 141 |
-
assertEqual(result.suggestions[0].end, 22, 'Suggestion Y end shifted 23→22 (-1)');
|
| 142 |
-
assertEqual(result.text.substring(result.suggestions[0].start, result.suggestions[0].end),
|
| 143 |
-
'والكلمه', 'Rebased offset Y extracts correct text');
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
// ══════════════════════════════════════════════════════════
|
| 147 |
-
// TEST 4: Multiple sequential applies
|
| 148 |
-
// ══════════════════════════════════════════════════════════
|
| 149 |
-
console.log('\n── TEST 4: Multiple sequential applies ──');
|
| 150 |
-
{
|
| 151 |
-
let text = TEXT;
|
| 152 |
-
let suggestions = makeSuggestions();
|
| 153 |
-
|
| 154 |
-
// Apply A first (same-length)
|
| 155 |
-
let r1 = applyAndRebase(text, suggestions[0], 'إلى', suggestions);
|
| 156 |
-
text = r1.text;
|
| 157 |
-
suggestions = r1.suggestions;
|
| 158 |
-
assertEqual(text, 'أنا ذاهب إلى البت والمدرسه', 'After apply A');
|
| 159 |
-
assert(suggestions.length === 2, '2 remaining after A');
|
| 160 |
-
|
| 161 |
-
// Apply B (longer, +2)
|
| 162 |
-
let r2 = applyAndRebase(text, suggestions[0], 'البيت', suggestions);
|
| 163 |
-
text = r2.text;
|
| 164 |
-
suggestions = r2.suggestions;
|
| 165 |
-
assertEqual(text, 'أنا ذاهب إلى البيت والمدرسه', 'After apply B');
|
| 166 |
-
assert(suggestions.length === 1, '1 remaining after B');
|
| 167 |
-
|
| 168 |
-
// Apply C (same-length) — offsets should be correctly rebased
|
| 169 |
-
let r3 = applyAndRebase(text, suggestions[0], 'المدرسة', suggestions);
|
| 170 |
-
text = r3.text;
|
| 171 |
-
suggestions = r3.suggestions;
|
| 172 |
-
assertEqual(text, 'أنا ذاهب إلى البيت والمدرسة', 'After apply C — fully corrected');
|
| 173 |
-
assert(suggestions.length === 0, '0 remaining — all applied');
|
| 174 |
-
|
| 175 |
-
// Verify final text matches applyAllPatches
|
| 176 |
-
const expectedFull = applyAllPatches(TEXT, makeSuggestions());
|
| 177 |
-
assertEqual(text, expectedFull, 'Sequential applies produce same result as applyAllPatches');
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
// ══════════════════════════════════════════════════════════
|
| 181 |
-
// TEST 5: Apply first suggestion (edge case)
|
| 182 |
-
// ══════════════════════════════════════════════════════════
|
| 183 |
-
console.log('\n── TEST 5: Apply first suggestion ──');
|
| 184 |
-
{
|
| 185 |
-
const text = 'الخطاء في البداية ثم نص صحيح';
|
| 186 |
-
const suggestions = [
|
| 187 |
-
{ id: 'f', start: 0, end: 6, original: 'الخطاء', correction: 'الخطأ', type: 'spelling' },
|
| 188 |
-
{ id: 'g', start: 20, end: 23, original: 'ثم ', correction: 'ثمّ ', type: 'grammar' },
|
| 189 |
-
];
|
| 190 |
-
|
| 191 |
-
const result = applyAndRebase(text, suggestions[0], 'الخطأ', suggestions);
|
| 192 |
-
assertEqual(result.text.substring(0, 5), 'الخطأ', 'First word replaced correctly');
|
| 193 |
-
// delta = 5 - 6 = -1
|
| 194 |
-
assertEqual(result.suggestions[0].start, 19, 'Second suggestion shifted by -1');
|
| 195 |
-
assertEqual(result.suggestions[0].end, 22, 'Second suggestion end shifted by -1');
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
// ══════════════════════════════════════════════════════════
|
| 199 |
-
// TEST 6: Apply last suggestion (edge case)
|
| 200 |
-
// ══════════════════════════════════════════���═══════════════
|
| 201 |
-
console.log('\n── TEST 6: Apply last suggestion ──');
|
| 202 |
-
{
|
| 203 |
-
const suggestions = makeSuggestions();
|
| 204 |
-
// Apply C (last one) — no suggestions after it to rebase
|
| 205 |
-
const result = applyAndRebase(TEXT, suggestions[2], 'المدرسة', suggestions);
|
| 206 |
-
|
| 207 |
-
assertEqual(result.text, 'أنا ذاهب الى البت والمدرسة', 'Last suggestion applied');
|
| 208 |
-
assert(result.suggestions.length === 2, '2 remaining');
|
| 209 |
-
assertEqual(result.suggestions[0].start, 10, 'A unchanged (before C)');
|
| 210 |
-
assertEqual(result.suggestions[1].start, 14, 'B unchanged (before C)');
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
// ══════════════════════════════════════════════════════════
|
| 214 |
-
// TEST 7: Dismiss (keep original)
|
| 215 |
-
// ══════════════════════════════════════════════════════════
|
| 216 |
-
console.log('\n── TEST 7: Dismiss (removeSuggestion) ──');
|
| 217 |
-
{
|
| 218 |
-
const suggestions = makeSuggestions();
|
| 219 |
-
const remaining = removeSuggestion(suggestions, 'b');
|
| 220 |
-
assert(remaining.length === 2, '2 remaining after dismiss');
|
| 221 |
-
assert(remaining.every(s => s.id !== 'b'), 'Dismissed suggestion removed');
|
| 222 |
-
// Offsets of remaining should be unchanged (no rebase on dismiss)
|
| 223 |
-
assertEqual(remaining[0].start, 10, 'A unchanged');
|
| 224 |
-
assertEqual(remaining[1].start, 19, 'C unchanged');
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
// ══════════════════════════════════════════════════════════
|
| 228 |
-
// TEST 8: applyAllPatches
|
| 229 |
-
// ══════════════════════════════════════════════════════════
|
| 230 |
-
console.log('\n── TEST 8: applyAllPatches ──');
|
| 231 |
-
{
|
| 232 |
-
const result = applyAllPatches(TEXT, makeSuggestions());
|
| 233 |
-
assertEqual(result, 'أنا ذاهب إلى البيت والمدرسة', 'All patches applied correctly');
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
// ══════════════════════════════════════════════════════════
|
| 237 |
-
// TEST 9: countByType
|
| 238 |
-
// ══════════════════════════════════════════════════════════
|
| 239 |
-
console.log('\n── TEST 9: countByType ──');
|
| 240 |
-
{
|
| 241 |
-
const counts = countByType(makeSuggestions());
|
| 242 |
-
assertEqual(counts.spelling, 2, '2 spelling');
|
| 243 |
-
assertEqual(counts.grammar, 1, '1 grammar');
|
| 244 |
-
assertEqual(counts.punctuation, 0, '0 punctuation');
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
// ══════════════════════════════════════════════════════════
|
| 248 |
-
// Summary
|
| 249 |
-
// ══════════════════════════════════════════════════════════
|
| 250 |
-
console.log(`\n${'═'.repeat(50)}`);
|
| 251 |
-
console.log(`Results: ${passed} passed, ${failed} failed`);
|
| 252 |
-
console.log(`${'═'.repeat(50)}`);
|
| 253 |
-
process.exit(failed > 0 ? 1 : 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension/tests/test_patches.ps1
DELETED
|
@@ -1,172 +0,0 @@
|
|
| 1 |
-
# Bayan Patch Engine Tests — PowerShell Runner
|
| 2 |
-
# Evaluates the JavaScript test logic without Node.js
|
| 3 |
-
|
| 4 |
-
$passed = 0
|
| 5 |
-
$failed = 0
|
| 6 |
-
|
| 7 |
-
function Assert-Equal($actual, $expected, $message) {
|
| 8 |
-
if ($actual -eq $expected) {
|
| 9 |
-
$script:passed++
|
| 10 |
-
Write-Host " ✅ $message" -ForegroundColor Green
|
| 11 |
-
} else {
|
| 12 |
-
$script:failed++
|
| 13 |
-
Write-Host " ❌ $message" -ForegroundColor Red
|
| 14 |
-
Write-Host " Expected: `"$expected`"" -ForegroundColor DarkGray
|
| 15 |
-
Write-Host " Actual: `"$actual`"" -ForegroundColor DarkGray
|
| 16 |
-
}
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
function Assert-True($condition, $message) {
|
| 20 |
-
if ($condition) {
|
| 21 |
-
$script:passed++
|
| 22 |
-
Write-Host " ✅ $message" -ForegroundColor Green
|
| 23 |
-
} else {
|
| 24 |
-
$script:failed++
|
| 25 |
-
Write-Host " ❌ $message" -ForegroundColor Red
|
| 26 |
-
}
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
# ── Simulate applyAndRebase in PowerShell ──
|
| 30 |
-
function ApplyAndRebase($text, $applied, $replacement, $suggestions) {
|
| 31 |
-
# Apply patch
|
| 32 |
-
$newText = $text.Substring(0, $applied.start) + $replacement + $text.Substring($applied.end)
|
| 33 |
-
|
| 34 |
-
# Calculate delta
|
| 35 |
-
$originalSpanLength = $applied.end - $applied.start
|
| 36 |
-
$delta = $replacement.Length - $originalSpanLength
|
| 37 |
-
|
| 38 |
-
# Remove applied and rebase
|
| 39 |
-
$rebased = @()
|
| 40 |
-
foreach ($s in $suggestions) {
|
| 41 |
-
if ($s.id -eq $applied.id) { continue }
|
| 42 |
-
if ($s.start -ge $applied.end) {
|
| 43 |
-
$rebased += @{
|
| 44 |
-
id = $s.id; start = $s.start + $delta; end = $s.end + $delta
|
| 45 |
-
original = $s.original; correction = $s.correction; type = $s.type
|
| 46 |
-
}
|
| 47 |
-
} else {
|
| 48 |
-
$rebased += $s
|
| 49 |
-
}
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
return @{ text = $newText; suggestions = $rebased }
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
function ApplyAllPatches($text, $suggestions) {
|
| 56 |
-
$sorted = $suggestions | Sort-Object { $_.start } -Descending
|
| 57 |
-
$result = $text
|
| 58 |
-
foreach ($s in $sorted) {
|
| 59 |
-
$result = $result.Substring(0, $s.start) + $s.correction + $result.Substring($s.end)
|
| 60 |
-
}
|
| 61 |
-
return $result
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
# ── Test Data ──
|
| 65 |
-
$TEXT = 'أنا ذاهب الى البت والمدرسه'
|
| 66 |
-
|
| 67 |
-
function MakeSuggestions {
|
| 68 |
-
return @(
|
| 69 |
-
@{ id='a'; start=10; end=13; original='الى'; correction='إلى'; type='grammar' },
|
| 70 |
-
@{ id='b'; start=14; end=17; original='البت'; correction='البيت'; type='spelling' },
|
| 71 |
-
@{ id='c'; start=19; end=26; original='المدرسه'; correction='المدرسة'; type='spelling' }
|
| 72 |
-
)
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
# ══════════════════════════════════════════════════════════
|
| 76 |
-
# TEST 1: Same-length replacement (delta = 0)
|
| 77 |
-
# ══════════════════════════════════════════════════════════
|
| 78 |
-
Write-Host "`n── TEST 1: Same-length replacement (delta = 0) ──" -ForegroundColor Cyan
|
| 79 |
-
$suggestions = MakeSuggestions
|
| 80 |
-
$result = ApplyAndRebase $TEXT $suggestions[0] 'إلى' $suggestions
|
| 81 |
-
Assert-Equal $result.text 'أنا ذاهب إلى البت والمدرسه' 'Text: الى → إلى'
|
| 82 |
-
Assert-True ($result.suggestions.Count -eq 2) 'Removed applied suggestion (2 remaining)'
|
| 83 |
-
Assert-Equal $result.suggestions[0].start 14 'Suggestion B start unchanged (14)'
|
| 84 |
-
Assert-Equal $result.suggestions[0].end 17 'Suggestion B end unchanged (17)'
|
| 85 |
-
Assert-Equal $result.suggestions[1].start 19 'Suggestion C start unchanged (19)'
|
| 86 |
-
Assert-Equal $result.suggestions[1].end 26 'Suggestion C end unchanged (26)'
|
| 87 |
-
|
| 88 |
-
# ══════════════════════════════════════════════════════════
|
| 89 |
-
# TEST 2: Longer replacement (delta > 0)
|
| 90 |
-
# ══════════════════════════════════════════════════════════
|
| 91 |
-
Write-Host "`n── TEST 2: Longer replacement (delta > 0) ──" -ForegroundColor Cyan
|
| 92 |
-
$suggestions = MakeSuggestions
|
| 93 |
-
$result = ApplyAndRebase $TEXT $suggestions[1] 'البيت' $suggestions
|
| 94 |
-
Assert-Equal $result.text 'أنا ذاهب الى البيت والمدرسه' 'Text: البت → البيت'
|
| 95 |
-
Assert-True ($result.suggestions.Count -eq 2) 'Removed applied suggestion (2 remaining)'
|
| 96 |
-
Assert-Equal $result.suggestions[0].start 10 'Suggestion A start unchanged (10)'
|
| 97 |
-
Assert-Equal $result.suggestions[0].end 13 'Suggestion A end unchanged (13)'
|
| 98 |
-
Assert-Equal $result.suggestions[1].start 21 'Suggestion C start shifted 19→21 (+2)'
|
| 99 |
-
Assert-Equal $result.suggestions[1].end 28 'Suggestion C end shifted 26→28 (+2)'
|
| 100 |
-
$extracted = $result.text.Substring($result.suggestions[1].start, $result.suggestions[1].end - $result.suggestions[1].start)
|
| 101 |
-
Assert-Equal $extracted 'المدرسه' 'Rebased offset C extracts correct text'
|
| 102 |
-
|
| 103 |
-
# ══════════════════════════════════════════════════════════
|
| 104 |
-
# TEST 3: Shorter replacement (delta < 0)
|
| 105 |
-
# ══════════════════════════════════════════════════════════
|
| 106 |
-
Write-Host "`n── TEST 3: Shorter replacement (delta < 0) ──" -ForegroundColor Cyan
|
| 107 |
-
$text3 = 'هذا النصص خاطئ والكلمه خطأ'
|
| 108 |
-
$sugs3 = @(
|
| 109 |
-
@{ id='x'; start=4; end=9; original='النصص'; correction='النص'; type='spelling' },
|
| 110 |
-
@{ id='y'; start=17; end=23; original='والكلمه'; correction='والكلمة'; type='spelling' }
|
| 111 |
-
)
|
| 112 |
-
$result = ApplyAndRebase $text3 $sugs3[0] 'النص' $sugs3
|
| 113 |
-
Assert-Equal $result.text 'هذا النص خاطئ والكلمه خطأ' 'Text: النصص → النص'
|
| 114 |
-
Assert-True ($result.suggestions.Count -eq 1) '1 remaining'
|
| 115 |
-
Assert-Equal $result.suggestions[0].start 16 'Suggestion Y start shifted 17→16 (-1)'
|
| 116 |
-
Assert-Equal $result.suggestions[0].end 22 'Suggestion Y end shifted 23→22 (-1)'
|
| 117 |
-
$extracted = $result.text.Substring($result.suggestions[0].start, $result.suggestions[0].end - $result.suggestions[0].start)
|
| 118 |
-
Assert-Equal $extracted 'والكلمه' 'Rebased offset Y extracts correct text'
|
| 119 |
-
|
| 120 |
-
# ══════════════════════════════════════════════════════════
|
| 121 |
-
# TEST 4: Multiple sequential applies
|
| 122 |
-
# ══════════════════════════════════════════════════════════
|
| 123 |
-
Write-Host "`n── TEST 4: Multiple sequential applies ──" -ForegroundColor Cyan
|
| 124 |
-
$text4 = $TEXT
|
| 125 |
-
$sugs4 = MakeSuggestions
|
| 126 |
-
$r1 = ApplyAndRebase $text4 $sugs4[0] 'إلى' $sugs4
|
| 127 |
-
$text4 = $r1.text; $sugs4 = $r1.suggestions
|
| 128 |
-
Assert-Equal $text4 'أنا ذاهب إلى البت والمدرسه' 'After apply A'
|
| 129 |
-
Assert-True ($sugs4.Count -eq 2) '2 remaining after A'
|
| 130 |
-
|
| 131 |
-
$r2 = ApplyAndRebase $text4 $sugs4[0] 'البيت' $sugs4
|
| 132 |
-
$text4 = $r2.text; $sugs4 = $r2.suggestions
|
| 133 |
-
Assert-Equal $text4 'أنا ذاهب إلى البيت والمدرسه' 'After apply B'
|
| 134 |
-
Assert-True ($sugs4.Count -eq 1) '1 remaining after B'
|
| 135 |
-
|
| 136 |
-
$r3 = ApplyAndRebase $text4 $sugs4[0] 'المدرسة' $sugs4
|
| 137 |
-
$text4 = $r3.text; $sugs4 = $r3.suggestions
|
| 138 |
-
Assert-Equal $text4 'أنا ذاهب إلى البيت والمدرسة' 'After apply C — fully corrected'
|
| 139 |
-
Assert-True ($sugs4.Count -eq 0) '0 remaining — all applied'
|
| 140 |
-
|
| 141 |
-
$expectedFull = ApplyAllPatches $TEXT (MakeSuggestions)
|
| 142 |
-
Assert-Equal $text4 $expectedFull 'Sequential === applyAllPatches'
|
| 143 |
-
|
| 144 |
-
# ══════════════════════════════════════════════════════════
|
| 145 |
-
# TEST 5: Apply last suggestion
|
| 146 |
-
# ══════════════════════════════════════════════════════════
|
| 147 |
-
Write-Host "`n── TEST 5: Apply last suggestion ──" -ForegroundColor Cyan
|
| 148 |
-
$sugs5 = MakeSuggestions
|
| 149 |
-
$result = ApplyAndRebase $TEXT $sugs5[2] 'المدرسة' $sugs5
|
| 150 |
-
Assert-Equal $result.text 'أنا ذاهب الى البت والمدرسة' 'Last suggestion applied'
|
| 151 |
-
Assert-True ($result.suggestions.Count -eq 2) '2 remaining'
|
| 152 |
-
Assert-Equal $result.suggestions[0].start 10 'A unchanged (before C)'
|
| 153 |
-
Assert-Equal $result.suggestions[1].start 14 'B unchanged (before C)'
|
| 154 |
-
|
| 155 |
-
# ══════════════════════════════════════════════════════════
|
| 156 |
-
# TEST 6: applyAllPatches
|
| 157 |
-
# ══════════════════════════════════════════════════════════
|
| 158 |
-
Write-Host "`n── TEST 6: applyAllPatches ──" -ForegroundColor Cyan
|
| 159 |
-
$resultAll = ApplyAllPatches $TEXT (MakeSuggestions)
|
| 160 |
-
Assert-Equal $resultAll 'أنا ذاهب إلى البيت والمدرسة' 'All patches applied correctly'
|
| 161 |
-
|
| 162 |
-
# ══════════════════════════════════════════════════════════
|
| 163 |
-
# Summary
|
| 164 |
-
# ══════════════════════════════════════════════════════════
|
| 165 |
-
Write-Host "`n$('═' * 50)"
|
| 166 |
-
if ($failed -gt 0) {
|
| 167 |
-
Write-Host "Results: $passed passed, $failed FAILED" -ForegroundColor Red
|
| 168 |
-
exit 1
|
| 169 |
-
} else {
|
| 170 |
-
Write-Host "Results: $passed passed, $failed failed" -ForegroundColor Green
|
| 171 |
-
exit 0
|
| 172 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension_divider.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
def insert_divider(file_path):
|
| 4 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 5 |
+
html = f.read()
|
| 6 |
+
|
| 7 |
+
# We look for the logo img and the span title, and insert a divider between them.
|
| 8 |
+
# Pattern to match:
|
| 9 |
+
# <img src="assets/icons/icon48.png" alt="بيان" width="28" height="28" style="border-radius:6px;">
|
| 10 |
+
# <span class="bayan-header-title">بيان</span>
|
| 11 |
+
|
| 12 |
+
# Let's match the closing bracket of the img tag (or its whitespace), followed by the span tag
|
| 13 |
+
pattern = r'(<img[^>]+>)\s*(<span class="(?:bayan-header-title|sp-header-title)">بيان</span>)'
|
| 14 |
+
|
| 15 |
+
# The divider to insert:
|
| 16 |
+
divider = '\n <div style="height:24px; width:2px; background-color:#d1d5db; border-radius:9999px; flex-shrink:0;"></div>\n '
|
| 17 |
+
|
| 18 |
+
# Replace
|
| 19 |
+
new_html, count = re.subn(pattern, r'\1' + divider + r'\2', html)
|
| 20 |
+
|
| 21 |
+
if count > 0:
|
| 22 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 23 |
+
f.write(new_html)
|
| 24 |
+
print(f"Updated {file_path}")
|
| 25 |
+
else:
|
| 26 |
+
print(f"No match found in {file_path}")
|
| 27 |
+
|
| 28 |
+
insert_divider('extension/popup.html')
|
| 29 |
+
insert_divider('extension/sidepanel/sidepanel.html')
|
fix_dividers.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
with open('src/index.html', 'r', encoding='utf-8') as f:
|
| 4 |
+
html = f.read()
|
| 5 |
+
|
| 6 |
+
# Replace the divider to use standard Tailwind classes and add shrink-0 so it doesn't disappear
|
| 7 |
+
old_div_nav = r'<div class="h-6 w-\[1\.5px\] bg-gray-300 dark:bg-gray-700 rounded-full"></div>'
|
| 8 |
+
new_div_nav = r'<div class="h-6 w-0.5 bg-gray-300 dark:bg-gray-600 rounded-full shrink-0"></div>'
|
| 9 |
+
|
| 10 |
+
old_div_footer = r'<div class="h-7 w-\[1\.5px\] bg-gray-300 dark:bg-gray-700 rounded-full"></div>'
|
| 11 |
+
new_div_footer = r'<div class="h-7 w-0.5 bg-gray-300 dark:bg-gray-600 rounded-full shrink-0"></div>'
|
| 12 |
+
|
| 13 |
+
html = re.sub(old_div_nav, new_div_nav, html)
|
| 14 |
+
html = re.sub(old_div_footer, new_div_footer, html)
|
| 15 |
+
|
| 16 |
+
with open('src/index.html', 'w', encoding='utf-8') as f:
|
| 17 |
+
f.write(html)
|
| 18 |
+
|
| 19 |
+
print("Dividers fixed.")
|
fix_listener.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
def fix_listener(filepath):
|
| 4 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 5 |
+
js = f.read()
|
| 6 |
+
|
| 7 |
+
# Replace the wrapping DOMContentLoaded for our appended theme toggle
|
| 8 |
+
pattern = r"// ── Theme Toggle Logic ──\s*document\.addEventListener\('DOMContentLoaded',\s*\(\)\s*=>\s*\{"
|
| 9 |
+
replacement = "// ── Theme Toggle Logic ──\n(function initBayanThemeToggle() {"
|
| 10 |
+
|
| 11 |
+
# We also need to replace the closing `});` of our block with `})();`
|
| 12 |
+
# We know it's at the very end of the file.
|
| 13 |
+
|
| 14 |
+
new_js, count = re.subn(pattern, replacement, js)
|
| 15 |
+
if count > 0:
|
| 16 |
+
# replace the last `});` with `})();`
|
| 17 |
+
new_js = new_js.rstrip()
|
| 18 |
+
if new_js.endswith("});"):
|
| 19 |
+
new_js = new_js[:-3] + "})();\n"
|
| 20 |
+
|
| 21 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 22 |
+
f.write(new_js)
|
| 23 |
+
print(f"Fixed in {filepath}")
|
| 24 |
+
else:
|
| 25 |
+
print(f"Not found in {filepath}")
|
| 26 |
+
|
| 27 |
+
fix_listener('extension/popup.js')
|
| 28 |
+
fix_listener('extension/sidepanel/sidepanel.js')
|
fix_sp_theme.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
sp_light_css = """
|
| 2 |
+
/* Light Theme Variables for Side Panel */
|
| 3 |
+
[data-theme="light"] {
|
| 4 |
+
--sp-bg: #f9fafb;
|
| 5 |
+
--sp-surface: #ffffff;
|
| 6 |
+
--sp-surface-hover: #f3f4f6;
|
| 7 |
+
--sp-surface-active: #e5e7eb;
|
| 8 |
+
--sp-border: #e5e7eb;
|
| 9 |
+
--sp-border-light: #d1d5db;
|
| 10 |
+
--sp-text: #111827;
|
| 11 |
+
--sp-text-secondary: #4b5563;
|
| 12 |
+
--sp-text-muted: #9ca3af;
|
| 13 |
+
--sp-success: #16a34a;
|
| 14 |
+
--sp-warning: #d97706;
|
| 15 |
+
}
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
with open('extension/sidepanel/sidepanel.css', 'a', encoding='utf-8') as f:
|
| 19 |
+
f.write('\n' + sp_light_css + '\n')
|
| 20 |
+
|
| 21 |
+
print("Fixed sidepanel CSS variables for light mode.")
|
fix_zindex.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
with open('src/index.html', 'r', encoding='utf-8') as f:
|
| 4 |
+
html = f.read()
|
| 5 |
+
|
| 6 |
+
# Replace z-50 with z-[1000] on the nav
|
| 7 |
+
html = re.sub(r'class="site-nav fixed top-0 right-0 left-0 z-50"', r'class="site-nav fixed top-0 right-0 left-0 z-[1000]"', html)
|
| 8 |
+
|
| 9 |
+
with open('src/index.html', 'w', encoding='utf-8') as f:
|
| 10 |
+
f.write(html)
|
| 11 |
+
|
| 12 |
+
print("z-index fixed.")
|
inject.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
with open('LOGOS/icon128.png', 'rb') as img:
|
| 5 |
+
data_uri = 'data:image/png;base64,' + base64.b64encode(img.read()).decode('utf-8')
|
| 6 |
+
|
| 7 |
+
with open('src/index.html', 'r', encoding='utf-8') as html_file:
|
| 8 |
+
html = html_file.read()
|
| 9 |
+
|
| 10 |
+
html = re.sub(r'src="data:image/png;base64,[A-Za-z0-9+/=]+"', 'src="' + data_uri + '"', html)
|
| 11 |
+
|
| 12 |
+
with open('src/index.html', 'w', encoding='utf-8') as html_file:
|
| 13 |
+
html_file.write(html)
|
inject_ext.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
with open('extension/assets/icons/icon128.png', 'rb') as img:
|
| 5 |
+
data_uri = 'data:image/png;base64,' + base64.b64encode(img.read()).decode('utf-8')
|
| 6 |
+
|
| 7 |
+
with open('src/index.html', 'r', encoding='utf-8') as f:
|
| 8 |
+
html = f.read()
|
| 9 |
+
|
| 10 |
+
html = re.sub(r'src="data:image/png;base64,[A-Za-z0-9+/=]+"', 'src="' + data_uri + '"', html)
|
| 11 |
+
|
| 12 |
+
with open('src/index.html', 'w', encoding='utf-8') as f:
|
| 13 |
+
f.write(html)
|
inject_logos.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import re
|
| 3 |
+
|
| 4 |
+
with open('LOGOS/icon128.png', 'rb') as img:
|
| 5 |
+
data_uri = 'data:image/png;base64,' + base64.b64encode(img.read()).decode('utf-8')
|
| 6 |
+
|
| 7 |
+
with open('src/index.html', 'r', encoding='utf-8') as f:
|
| 8 |
+
html = f.read()
|
| 9 |
+
|
| 10 |
+
html = re.sub(r'src="data:image/png;base64,[A-Za-z0-9+/=]+"', 'src="' + data_uri + '"', html)
|
| 11 |
+
|
| 12 |
+
with open('src/index.html', 'w', encoding='utf-8') as f:
|
| 13 |
+
f.write(html)
|
src/app.py
CHANGED
|
@@ -87,12 +87,23 @@ logging.basicConfig(
|
|
| 87 |
)
|
| 88 |
logger = logging.getLogger(__name__)
|
| 89 |
|
| 90 |
-
DEBUG_TRACE =
|
| 91 |
|
| 92 |
# Initialize Flask app
|
| 93 |
-
app = Flask(__name__, static_folder='.',
|
| 94 |
CORS(app, resources={r"/api/*": {"origins": "*"}}) # CORS for API routes only
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
# Configuration
|
| 97 |
MAX_TEXT_LENGTH = 5000 # Maximum characters for input text
|
| 98 |
MAX_SUMMARY_LENGTH = 512 # Maximum tokens for summary
|
|
@@ -186,11 +197,11 @@ def index():
|
|
| 186 |
# Inject Supabase credentials into the meta tags
|
| 187 |
html = html.replace(
|
| 188 |
'<meta name="supabase-url" content="">',
|
| 189 |
-
f'<meta name="supabase-url" content="{SUPABASE_URL}">'
|
| 190 |
)
|
| 191 |
html = html.replace(
|
| 192 |
'<meta name="supabase-anon-key" content="">',
|
| 193 |
-
f'<meta name="supabase-anon-key" content="{SUPABASE_ANON_KEY}">'
|
| 194 |
)
|
| 195 |
|
| 196 |
return Response(html, mimetype='text/html')
|
|
@@ -243,6 +254,8 @@ def health_check():
|
|
| 243 |
@app.route('/api/debug-models', methods=['GET'])
|
| 244 |
def debug_models():
|
| 245 |
"""Debug endpoint: report model status and startup errors."""
|
|
|
|
|
|
|
| 246 |
from hf_inference import debug_test_all_models
|
| 247 |
results = debug_test_all_models()
|
| 248 |
|
|
@@ -307,8 +320,8 @@ def _punctuation_available():
|
|
| 307 |
def _autocomplete_available():
|
| 308 |
"""Check if autocomplete model is loaded (without triggering lazy load)."""
|
| 309 |
try:
|
| 310 |
-
from nlp.autocomplete.autocomplete_service import
|
| 311 |
-
return
|
| 312 |
except Exception:
|
| 313 |
return False
|
| 314 |
|
|
@@ -620,6 +633,12 @@ def add_punctuation():
|
|
| 620 |
if not text:
|
| 621 |
return jsonify({'error': 'Text is required', 'status': 'error'}), 400
|
| 622 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
logger.info(f"Adding punctuation for text of length: {len(text)}")
|
| 624 |
from nlp.punctuation.punctuation_service import get_punctuation_model
|
| 625 |
punc_checker = get_punctuation_model()
|
|
@@ -1620,7 +1639,7 @@ def analyze_text():
|
|
| 1620 |
text_len = len(current_text)
|
| 1621 |
run_spelling = text_len <= 1000 # FIX-10: Increased from 300 to 1000
|
| 1622 |
if not run_spelling:
|
| 1623 |
-
logger.info(f"[ANALYZE] Text length {text_len} >
|
| 1624 |
|
| 1625 |
# ── Batch 2+5: Religious text detection (moved before spelling) ──
|
| 1626 |
# Religious text must skip ALL stages (spelling + grammar + punctuation)
|
|
@@ -1663,7 +1682,7 @@ def analyze_text():
|
|
| 1663 |
_has_percent = bool(_re_spell_guard.search(r'\d+\.\d+%', ctx.current_text))
|
| 1664 |
_has_latin_word = bool(_re_spell_guard.search(r'\b[A-Za-z]{3,}\b', ctx.current_text))
|
| 1665 |
|
| 1666 |
-
_skip_all_stages = _is_religious_text
|
| 1667 |
if _has_url or _has_email:
|
| 1668 |
logger.info(f"[ANALYZE] Text contains URLs/emails — skipping spelling")
|
| 1669 |
run_spelling = False
|
|
@@ -2059,6 +2078,12 @@ def analyze_text():
|
|
| 2059 |
_oov_words = _oov_text.split()
|
| 2060 |
_oov_changed = False
|
| 2061 |
_oov_result = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2062 |
|
| 2063 |
for _ow_idx, _ow in enumerate(_oov_words):
|
| 2064 |
# Skip short words (prepositions etc.)
|
|
@@ -2086,7 +2111,7 @@ def analyze_text():
|
|
| 2086 |
)
|
| 2087 |
_oov_result.append(_ta_cand + _punct_suffix)
|
| 2088 |
_oov_changed = True
|
| 2089 |
-
_ow_pos =
|
| 2090 |
if _ow_pos + len(_ow) <= len(_oov_text):
|
| 2091 |
ctx.add_patch(
|
| 2092 |
'spelling', _ow_pos, _ow_pos + len(_ow),
|
|
@@ -2105,7 +2130,7 @@ def analyze_text():
|
|
| 2105 |
)
|
| 2106 |
_oov_result.append(_wo_cand + _punct_suffix)
|
| 2107 |
_oov_changed = True
|
| 2108 |
-
_ow_pos =
|
| 2109 |
if _ow_pos + len(_ow) <= len(_oov_text):
|
| 2110 |
ctx.add_patch(
|
| 2111 |
'spelling', _ow_pos, _ow_pos + len(_ow),
|
|
@@ -2122,7 +2147,7 @@ def analyze_text():
|
|
| 2122 |
)
|
| 2123 |
_oov_result.append(_woa_cand + _punct_suffix)
|
| 2124 |
_oov_changed = True
|
| 2125 |
-
_ow_pos =
|
| 2126 |
if _ow_pos + len(_ow) <= len(_oov_text):
|
| 2127 |
ctx.add_patch(
|
| 2128 |
'spelling', _ow_pos, _ow_pos + len(_ow),
|
|
@@ -2141,7 +2166,7 @@ def analyze_text():
|
|
| 2141 |
)
|
| 2142 |
_oov_result.append(_dotwo_clean + '.')
|
| 2143 |
_oov_changed = True
|
| 2144 |
-
_ow_pos =
|
| 2145 |
if _ow_pos + len(_ow) <= len(_oov_text):
|
| 2146 |
ctx.add_patch(
|
| 2147 |
'spelling', _ow_pos, _ow_pos + len(_ow),
|
|
@@ -2192,8 +2217,8 @@ def analyze_text():
|
|
| 2192 |
# Replace structured content with Arabic placeholder tokens
|
| 2193 |
if _structured_placeholders:
|
| 2194 |
_structured_placeholders.sort(key=lambda x: x[0], reverse=True)
|
| 2195 |
-
for _sp_start, _sp_end, _sp_text in _structured_placeholders:
|
| 2196 |
-
_grammar_input_text = _grammar_input_text[:_sp_start] + 'بيان' + _grammar_input_text[_sp_end:]
|
| 2197 |
logger.info(f"[ANALYZE] Protected {len(_structured_placeholders)} structured elements")
|
| 2198 |
|
| 2199 |
# 2. Grammar (runs on spelling-corrected text — word-level dependency)
|
|
@@ -2214,9 +2239,8 @@ def analyze_text():
|
|
| 2214 |
|
| 2215 |
# FIX-03: Restore structured content in grammar output
|
| 2216 |
if _structured_placeholders:
|
| 2217 |
-
|
| 2218 |
-
|
| 2219 |
-
corrected_grammar = corrected_grammar.replace('بيان', _sp_text, 1)
|
| 2220 |
|
| 2221 |
if corrected_grammar != ctx.current_text:
|
| 2222 |
diffs = get_word_diffs(ctx.current_text, corrected_grammar)
|
|
@@ -2354,9 +2378,10 @@ def analyze_text():
|
|
| 2354 |
# - Title case Latin words (proper nouns in mixed text)
|
| 2355 |
# - Single words where the grammar just adds/removes spaces
|
| 2356 |
if orig_text and corr_text:
|
| 2357 |
-
|
| 2358 |
-
|
| 2359 |
-
|
|
|
|
| 2360 |
logger.info(
|
| 2361 |
f"[GRAMMAR] Skipping entity (contains Latin): "
|
| 2362 |
f"'{orig_text}'→'{corr_text}'"
|
|
@@ -2520,7 +2545,7 @@ def analyze_text():
|
|
| 2520 |
_gram_dir_blocked = True
|
| 2521 |
break
|
| 2522 |
if _gram_dir_blocked:
|
| 2523 |
-
logger.
|
| 2524 |
continue
|
| 2525 |
# DEBUG_TRACE
|
| 2526 |
if _is_grammar_pattern:
|
|
@@ -2533,6 +2558,7 @@ def analyze_text():
|
|
| 2533 |
for _tc in _TANWEEN_CHARS:
|
| 2534 |
if _tc in orig_text and _tc not in corr_text:
|
| 2535 |
corr_text = corr_text + _tc
|
|
|
|
| 2536 |
break
|
| 2537 |
|
| 2538 |
# Re-label: if grammar's change is purely orthographic
|
|
@@ -2545,7 +2571,7 @@ def analyze_text():
|
|
| 2545 |
_tel_events.append({"event":"patch_accepted","stage":stage_label,"original":orig_text[:80],"correction":corr_text[:80],"start":d["start"],"end":d["end"]})
|
| 2546 |
ctx.add_patch(
|
| 2547 |
stage_label, d['start'], d['end'],
|
| 2548 |
-
corr_text, confidence=
|
| 2549 |
)
|
| 2550 |
|
| 2551 |
# ── B7 (E6): Bracket-balance guard ──
|
|
@@ -2560,11 +2586,11 @@ def analyze_text():
|
|
| 2560 |
corr_balanced = (corr_opens == corr_closes)
|
| 2561 |
if orig_balanced and not corr_balanced:
|
| 2562 |
logger.info(
|
| 2563 |
-
f"[GRAMMAR]
|
| 2564 |
-
f"orig=({orig_opens},{orig_closes}), corr=({corr_opens},{corr_closes})"
|
|
|
|
| 2565 |
)
|
| 2566 |
-
|
| 2567 |
-
elif _grammar_accepted_diffs:
|
| 2568 |
# FIX-04: Rebuild grammar text from ACCEPTED diffs only,
|
| 2569 |
# not the full model output. Prevents phantom corrections.
|
| 2570 |
_safe_grammar = ctx.current_text
|
|
@@ -2647,7 +2673,7 @@ def analyze_text():
|
|
| 2647 |
to_remove = set(id(p) for p in punc_patches_sorted[MAX_PUNC_PATCHES_PER_RESPONSE:])
|
| 2648 |
# FIX-18: Also remove StageLocker locks for capped patches
|
| 2649 |
for _capped_p in punc_patches_sorted[MAX_PUNC_PATCHES_PER_RESPONSE:]:
|
| 2650 |
-
ctx.stage_locker.unlock(_capped_p.
|
| 2651 |
ctx.patches.patches = [p for p in ctx.patches.patches if id(p) not in to_remove]
|
| 2652 |
logger.info(
|
| 2653 |
f"[PUNC-CAP] Capped punctuation patches: "
|
|
@@ -2743,7 +2769,6 @@ def analyze_text():
|
|
| 2743 |
'suggestions': suggestions,
|
| 2744 |
'timing_ms': timing_ms,
|
| 2745 |
'status': response_status,
|
| 2746 |
-
'telemetry': _tel_events,
|
| 2747 |
}
|
| 2748 |
if stage_errors:
|
| 2749 |
response_data['warnings'] = stage_errors
|
|
|
|
| 87 |
)
|
| 88 |
logger = logging.getLogger(__name__)
|
| 89 |
|
| 90 |
+
DEBUG_TRACE = os.environ.get('BAYAN_DEBUG_TRACE', '').lower() in ('1', 'true') # Enable with env var
|
| 91 |
|
| 92 |
# Initialize Flask app
|
| 93 |
+
app = Flask(__name__, static_folder='.', static_url_path='')
|
| 94 |
CORS(app, resources={r"/api/*": {"origins": "*"}}) # CORS for API routes only
|
| 95 |
|
| 96 |
+
_ALLOWED_STATIC_EXTENSIONS = {'.css', '.js', '.svg', '.png', '.ico', '.woff', '.woff2', '.ttf', '.jpg', '.jpeg', '.gif', '.webp', '.map'}
|
| 97 |
+
|
| 98 |
+
@app.before_request
|
| 99 |
+
def _block_source_files():
|
| 100 |
+
from flask import request as _req, abort as _abort
|
| 101 |
+
if _req.path.startswith('/api/') or _req.path == '/':
|
| 102 |
+
return None
|
| 103 |
+
ext = os.path.splitext(_req.path)[1].lower()
|
| 104 |
+
if ext and ext not in _ALLOWED_STATIC_EXTENSIONS:
|
| 105 |
+
_abort(404)
|
| 106 |
+
|
| 107 |
# Configuration
|
| 108 |
MAX_TEXT_LENGTH = 5000 # Maximum characters for input text
|
| 109 |
MAX_SUMMARY_LENGTH = 512 # Maximum tokens for summary
|
|
|
|
| 197 |
# Inject Supabase credentials into the meta tags
|
| 198 |
html = html.replace(
|
| 199 |
'<meta name="supabase-url" content="">',
|
| 200 |
+
f'<meta name="supabase-url" content="{__import__("html").escape(SUPABASE_URL or "")}">'
|
| 201 |
)
|
| 202 |
html = html.replace(
|
| 203 |
'<meta name="supabase-anon-key" content="">',
|
| 204 |
+
f'<meta name="supabase-anon-key" content="{__import__("html").escape(SUPABASE_ANON_KEY or "")}">'
|
| 205 |
)
|
| 206 |
|
| 207 |
return Response(html, mimetype='text/html')
|
|
|
|
| 254 |
@app.route('/api/debug-models', methods=['GET'])
|
| 255 |
def debug_models():
|
| 256 |
"""Debug endpoint: report model status and startup errors."""
|
| 257 |
+
if not app.debug:
|
| 258 |
+
return jsonify({'error': 'Not available in production'}), 403
|
| 259 |
from hf_inference import debug_test_all_models
|
| 260 |
results = debug_test_all_models()
|
| 261 |
|
|
|
|
| 320 |
def _autocomplete_available():
|
| 321 |
"""Check if autocomplete model is loaded (without triggering lazy load)."""
|
| 322 |
try:
|
| 323 |
+
from nlp.autocomplete.autocomplete_service import is_loaded
|
| 324 |
+
return is_loaded()
|
| 325 |
except Exception:
|
| 326 |
return False
|
| 327 |
|
|
|
|
| 633 |
if not text:
|
| 634 |
return jsonify({'error': 'Text is required', 'status': 'error'}), 400
|
| 635 |
|
| 636 |
+
if len(text) > MAX_TEXT_LENGTH:
|
| 637 |
+
return jsonify({
|
| 638 |
+
'error': f'Text too long. Maximum {MAX_TEXT_LENGTH} characters.',
|
| 639 |
+
'status': 'error'
|
| 640 |
+
}), 400
|
| 641 |
+
|
| 642 |
logger.info(f"Adding punctuation for text of length: {len(text)}")
|
| 643 |
from nlp.punctuation.punctuation_service import get_punctuation_model
|
| 644 |
punc_checker = get_punctuation_model()
|
|
|
|
| 1639 |
text_len = len(current_text)
|
| 1640 |
run_spelling = text_len <= 1000 # FIX-10: Increased from 300 to 1000
|
| 1641 |
if not run_spelling:
|
| 1642 |
+
logger.info(f"[ANALYZE] Text length {text_len} > 1000 — skipping AraSpell for performance")
|
| 1643 |
|
| 1644 |
# ── Batch 2+5: Religious text detection (moved before spelling) ──
|
| 1645 |
# Religious text must skip ALL stages (spelling + grammar + punctuation)
|
|
|
|
| 1682 |
_has_percent = bool(_re_spell_guard.search(r'\d+\.\d+%', ctx.current_text))
|
| 1683 |
_has_latin_word = bool(_re_spell_guard.search(r'\b[A-Za-z]{3,}\b', ctx.current_text))
|
| 1684 |
|
| 1685 |
+
_skip_all_stages = _is_religious_text
|
| 1686 |
if _has_url or _has_email:
|
| 1687 |
logger.info(f"[ANALYZE] Text contains URLs/emails — skipping spelling")
|
| 1688 |
run_spelling = False
|
|
|
|
| 2078 |
_oov_words = _oov_text.split()
|
| 2079 |
_oov_changed = False
|
| 2080 |
_oov_result = []
|
| 2081 |
+
_oov_word_starts = []
|
| 2082 |
+
_oov_search = 0
|
| 2083 |
+
for _ow_w in _oov_words:
|
| 2084 |
+
_ow_found = _oov_text.find(_ow_w, _oov_search)
|
| 2085 |
+
_oov_word_starts.append(_ow_found if _ow_found >= 0 else _oov_search)
|
| 2086 |
+
_oov_search = (_ow_found if _ow_found >= 0 else _oov_search) + len(_ow_w)
|
| 2087 |
|
| 2088 |
for _ow_idx, _ow in enumerate(_oov_words):
|
| 2089 |
# Skip short words (prepositions etc.)
|
|
|
|
| 2111 |
)
|
| 2112 |
_oov_result.append(_ta_cand + _punct_suffix)
|
| 2113 |
_oov_changed = True
|
| 2114 |
+
_ow_pos = _oov_word_starts[_ow_idx]
|
| 2115 |
if _ow_pos + len(_ow) <= len(_oov_text):
|
| 2116 |
ctx.add_patch(
|
| 2117 |
'spelling', _ow_pos, _ow_pos + len(_ow),
|
|
|
|
| 2130 |
)
|
| 2131 |
_oov_result.append(_wo_cand + _punct_suffix)
|
| 2132 |
_oov_changed = True
|
| 2133 |
+
_ow_pos = _oov_word_starts[_ow_idx]
|
| 2134 |
if _ow_pos + len(_ow) <= len(_oov_text):
|
| 2135 |
ctx.add_patch(
|
| 2136 |
'spelling', _ow_pos, _ow_pos + len(_ow),
|
|
|
|
| 2147 |
)
|
| 2148 |
_oov_result.append(_woa_cand + _punct_suffix)
|
| 2149 |
_oov_changed = True
|
| 2150 |
+
_ow_pos = _oov_word_starts[_ow_idx]
|
| 2151 |
if _ow_pos + len(_ow) <= len(_oov_text):
|
| 2152 |
ctx.add_patch(
|
| 2153 |
'spelling', _ow_pos, _ow_pos + len(_ow),
|
|
|
|
| 2166 |
)
|
| 2167 |
_oov_result.append(_dotwo_clean + '.')
|
| 2168 |
_oov_changed = True
|
| 2169 |
+
_ow_pos = _oov_word_starts[_ow_idx]
|
| 2170 |
if _ow_pos + len(_ow) <= len(_oov_text):
|
| 2171 |
ctx.add_patch(
|
| 2172 |
'spelling', _ow_pos, _ow_pos + len(_ow),
|
|
|
|
| 2217 |
# Replace structured content with Arabic placeholder tokens
|
| 2218 |
if _structured_placeholders:
|
| 2219 |
_structured_placeholders.sort(key=lambda x: x[0], reverse=True)
|
| 2220 |
+
for _sp_idx, (_sp_start, _sp_end, _sp_text) in enumerate(_structured_placeholders):
|
| 2221 |
+
_grammar_input_text = _grammar_input_text[:_sp_start] + f'بيان{_sp_idx}' + _grammar_input_text[_sp_end:]
|
| 2222 |
logger.info(f"[ANALYZE] Protected {len(_structured_placeholders)} structured elements")
|
| 2223 |
|
| 2224 |
# 2. Grammar (runs on spelling-corrected text — word-level dependency)
|
|
|
|
| 2239 |
|
| 2240 |
# FIX-03: Restore structured content in grammar output
|
| 2241 |
if _structured_placeholders:
|
| 2242 |
+
for _sp_idx, (_sp_start, _sp_end, _sp_text) in enumerate(_structured_placeholders):
|
| 2243 |
+
corrected_grammar = corrected_grammar.replace(f'بيان{_sp_idx}', _sp_text)
|
|
|
|
| 2244 |
|
| 2245 |
if corrected_grammar != ctx.current_text:
|
| 2246 |
diffs = get_word_diffs(ctx.current_text, corrected_grammar)
|
|
|
|
| 2378 |
# - Title case Latin words (proper nouns in mixed text)
|
| 2379 |
# - Single words where the grammar just adds/removes spaces
|
| 2380 |
if orig_text and corr_text:
|
| 2381 |
+
import re as _re_latin
|
| 2382 |
+
_orig_latin = _re_latin.sub(r'[^A-Za-z]', '', orig_text)
|
| 2383 |
+
_corr_latin = _re_latin.sub(r'[^A-Za-z]', '', corr_text)
|
| 2384 |
+
if _orig_latin and _orig_latin != _corr_latin:
|
| 2385 |
logger.info(
|
| 2386 |
f"[GRAMMAR] Skipping entity (contains Latin): "
|
| 2387 |
f"'{orig_text}'→'{corr_text}'"
|
|
|
|
| 2545 |
_gram_dir_blocked = True
|
| 2546 |
break
|
| 2547 |
if _gram_dir_blocked:
|
| 2548 |
+
logger.info(f"[GRAMMAR] Directional block rejected diff: '{orig_text}'→'{corr_text}'")
|
| 2549 |
continue
|
| 2550 |
# DEBUG_TRACE
|
| 2551 |
if _is_grammar_pattern:
|
|
|
|
| 2558 |
for _tc in _TANWEEN_CHARS:
|
| 2559 |
if _tc in orig_text and _tc not in corr_text:
|
| 2560 |
corr_text = corr_text + _tc
|
| 2561 |
+
d['correction'] = corr_text
|
| 2562 |
break
|
| 2563 |
|
| 2564 |
# Re-label: if grammar's change is purely orthographic
|
|
|
|
| 2571 |
_tel_events.append({"event":"patch_accepted","stage":stage_label,"original":orig_text[:80],"correction":corr_text[:80],"start":d["start"],"end":d["end"]})
|
| 2572 |
ctx.add_patch(
|
| 2573 |
stage_label, d['start'], d['end'],
|
| 2574 |
+
corr_text, confidence=0.85
|
| 2575 |
)
|
| 2576 |
|
| 2577 |
# ── B7 (E6): Bracket-balance guard ──
|
|
|
|
| 2586 |
corr_balanced = (corr_opens == corr_closes)
|
| 2587 |
if orig_balanced and not corr_balanced:
|
| 2588 |
logger.info(
|
| 2589 |
+
f"[GRAMMAR] Bracket-unbalanced output detected: "
|
| 2590 |
+
f"orig=({orig_opens},{orig_closes}), corr=({corr_opens},{corr_closes}). "
|
| 2591 |
+
f"Using individually-accepted diffs only."
|
| 2592 |
)
|
| 2593 |
+
if _grammar_accepted_diffs:
|
|
|
|
| 2594 |
# FIX-04: Rebuild grammar text from ACCEPTED diffs only,
|
| 2595 |
# not the full model output. Prevents phantom corrections.
|
| 2596 |
_safe_grammar = ctx.current_text
|
|
|
|
| 2673 |
to_remove = set(id(p) for p in punc_patches_sorted[MAX_PUNC_PATCHES_PER_RESPONSE:])
|
| 2674 |
# FIX-18: Also remove StageLocker locks for capped patches
|
| 2675 |
for _capped_p in punc_patches_sorted[MAX_PUNC_PATCHES_PER_RESPONSE:]:
|
| 2676 |
+
ctx.stage_locker.unlock(_capped_p.start_current, _capped_p.end_current)
|
| 2677 |
ctx.patches.patches = [p for p in ctx.patches.patches if id(p) not in to_remove]
|
| 2678 |
logger.info(
|
| 2679 |
f"[PUNC-CAP] Capped punctuation patches: "
|
|
|
|
| 2769 |
'suggestions': suggestions,
|
| 2770 |
'timing_ms': timing_ms,
|
| 2771 |
'status': response_status,
|
|
|
|
| 2772 |
}
|
| 2773 |
if stage_errors:
|
| 2774 |
response_data['warnings'] = stage_errors
|
src/favicon.png
ADDED
|
|
Git LFS Details
|
src/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 12 |
<meta property="og:description" content="تدقيق إملائي ونحوي وترقيم، تلخيص ذكي، إكمال تلقائي، تدقيق القرآن الكريم، وتحويل اللهجات">
|
| 13 |
<meta name="twitter:card" content="summary_large_image">
|
| 14 |
<meta name="twitter:image" content="https://raw.githubusercontent.com/mohamedatef24/BAYAN/main/extension/assets/icons/icon128.png">
|
| 15 |
-
<link rel="icon" type="image/png" href="
|
| 16 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 17 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 18 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -847,9 +847,9 @@
|
|
| 847 |
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: var(--color-primary);"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
| 848 |
<span class="text-base font-bold">تلخيص النصوص</span>
|
| 849 |
</div>
|
| 850 |
-
<textarea id="summary-custom-input" class="w-full p-4 rounded-xl text-right text-lg editor-content" dir="rtl" rows="6" placeholder="الصق أو اكتب النص الذي تريد تلخيصه هنا..." style="background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-text-primary); resize: vertical; font-family: inherit;"></textarea>
|
| 851 |
<div class="flex items-center justify-between mt-2 mb-2" dir="rtl">
|
| 852 |
-
<span id="summary-char-count" class="text-xs" style="color: var(--text-secondary);">
|
| 853 |
<label class="btn-ghost" style="cursor:pointer; display:inline-flex; align-items:center; gap:5px; padding:4px 12px; min-height:32px; font-size:12px;">
|
| 854 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
|
| 855 |
استيراد ملف
|
|
@@ -915,7 +915,7 @@
|
|
| 915 |
</div>
|
| 916 |
<textarea id="dialect-input" class="w-full p-4 rounded-xl text-right text-lg editor-content" dir="rtl" rows="6" placeholder="اكتب النص باللهجة هنا..." oninput="updateDialectCharCount()" style="background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-text-primary); resize: vertical; font-family: inherit;"></textarea>
|
| 917 |
<div class="flex items-center justify-between mt-1 mb-2" dir="rtl">
|
| 918 |
-
<span id="dialect-char-count" class="text-xs" style="color: var(--text-secondary);">٠
|
| 919 |
</div>
|
| 920 |
<button id="dialect-convert-btn" onclick="convertDialect()" class="btn-primary w-full py-4 text-lg mt-4 mb-4" type="button">تحويل إلى الفصحى</button>
|
| 921 |
<div id="dialect-result-card" class="is-hidden" style="background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 1rem; padding: 1.5rem;">
|
|
@@ -1675,13 +1675,22 @@
|
|
| 1675 |
if (typeof showToast === 'function') showToast('✓ تم تطبيق النص الفصيح في المحرر');
|
| 1676 |
}
|
| 1677 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1678 |
function updateDialectCharCount() {
|
| 1679 |
var input = document.getElementById('dialect-input');
|
| 1680 |
var counter = document.getElementById('dialect-char-count');
|
| 1681 |
if (!input || !counter) return;
|
| 1682 |
var len = input.value.length;
|
| 1683 |
var arabicLen = len.toLocaleString('ar-EG');
|
| 1684 |
-
counter.textContent = arabicLen + '
|
| 1685 |
counter.style.color = len > 5000 ? '#ef4444' : 'var(--text-secondary)';
|
| 1686 |
}
|
| 1687 |
|
|
|
|
| 12 |
<meta property="og:description" content="تدقيق إملائي ونحوي وترقيم، تلخيص ذكي، إكمال تلقائي، تدقيق القرآن الكريم، وتحويل اللهجات">
|
| 13 |
<meta name="twitter:card" content="summary_large_image">
|
| 14 |
<meta name="twitter:image" content="https://raw.githubusercontent.com/mohamedatef24/BAYAN/main/extension/assets/icons/icon128.png">
|
| 15 |
+
<link rel="icon" type="image/png" href="favicon.png">
|
| 16 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 17 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 18 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
|
| 847 |
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: var(--color-primary);"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
| 848 |
<span class="text-base font-bold">تلخيص النصوص</span>
|
| 849 |
</div>
|
| 850 |
+
<textarea id="summary-custom-input" class="w-full p-4 rounded-xl text-right text-lg editor-content" dir="rtl" rows="6" placeholder="الصق أو اكتب النص الذي تريد تلخيصه هنا..." oninput="updateSummaryWordCount()" style="background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-text-primary); resize: vertical; font-family: inherit;"></textarea>
|
| 851 |
<div class="flex items-center justify-between mt-2 mb-2" dir="rtl">
|
| 852 |
+
<span id="summary-char-count" class="text-xs" style="color: var(--text-secondary);">٠ كلمة</span>
|
| 853 |
<label class="btn-ghost" style="cursor:pointer; display:inline-flex; align-items:center; gap:5px; padding:4px 12px; min-height:32px; font-size:12px;">
|
| 854 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
|
| 855 |
استيراد ملف
|
|
|
|
| 915 |
</div>
|
| 916 |
<textarea id="dialect-input" class="w-full p-4 rounded-xl text-right text-lg editor-content" dir="rtl" rows="6" placeholder="اكتب النص باللهجة هنا..." oninput="updateDialectCharCount()" style="background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-text-primary); resize: vertical; font-family: inherit;"></textarea>
|
| 917 |
<div class="flex items-center justify-between mt-1 mb-2" dir="rtl">
|
| 918 |
+
<span id="dialect-char-count" class="text-xs" style="color: var(--text-secondary);">٠ حرف</span>
|
| 919 |
</div>
|
| 920 |
<button id="dialect-convert-btn" onclick="convertDialect()" class="btn-primary w-full py-4 text-lg mt-4 mb-4" type="button">تحويل إلى الفصحى</button>
|
| 921 |
<div id="dialect-result-card" class="is-hidden" style="background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 1rem; padding: 1.5rem;">
|
|
|
|
| 1675 |
if (typeof showToast === 'function') showToast('✓ تم تطبيق النص الفصيح في المحرر');
|
| 1676 |
}
|
| 1677 |
|
| 1678 |
+
function updateSummaryWordCount() {
|
| 1679 |
+
var input = document.getElementById('summary-custom-input');
|
| 1680 |
+
var counter = document.getElementById('summary-char-count');
|
| 1681 |
+
if (!input || !counter) return;
|
| 1682 |
+
var text = input.value.trim();
|
| 1683 |
+
var words = text ? text.split(/\s+/).length : 0;
|
| 1684 |
+
counter.textContent = words.toLocaleString('ar-EG') + ' كلمة';
|
| 1685 |
+
}
|
| 1686 |
+
|
| 1687 |
function updateDialectCharCount() {
|
| 1688 |
var input = document.getElementById('dialect-input');
|
| 1689 |
var counter = document.getElementById('dialect-char-count');
|
| 1690 |
if (!input || !counter) return;
|
| 1691 |
var len = input.value.length;
|
| 1692 |
var arabicLen = len.toLocaleString('ar-EG');
|
| 1693 |
+
counter.textContent = arabicLen + ' حرف';
|
| 1694 |
counter.style.color = len > 5000 ? '#ef4444' : 'var(--text-secondary)';
|
| 1695 |
}
|
| 1696 |
|
src/js/format.js
CHANGED
|
@@ -393,7 +393,8 @@ function updateEnhancedStats() {
|
|
| 393 |
|
| 394 |
/* ── Item 6: Summary Stats ── */
|
| 395 |
function updateSummaryStats(summaryText) {
|
| 396 |
-
const
|
|
|
|
| 397 |
const summaryWords = summaryText.trim().split(/\s+/).filter(w => w.length > 0).length;
|
| 398 |
const originalWords = originalText.trim().split(/\s+/).filter(w => w.length > 0).length;
|
| 399 |
const compression = originalWords > 0 ? Math.round((1 - summaryWords / originalWords) * 100) : 0;
|
|
|
|
| 393 |
|
| 394 |
/* ── Item 6: Summary Stats ── */
|
| 395 |
function updateSummaryStats(summaryText) {
|
| 396 |
+
const customInput = document.getElementById('summary-custom-input');
|
| 397 |
+
const originalText = customInput ? customInput.value : getEditorText();
|
| 398 |
const summaryWords = summaryText.trim().split(/\s+/).filter(w => w.length > 0).length;
|
| 399 |
const originalWords = originalText.trim().split(/\s+/).filter(w => w.length > 0).length;
|
| 400 |
const compression = originalWords > 0 ? Math.round((1 - summaryWords / originalWords) * 100) : 0;
|
src/nlp/autocomplete/autocomplete_service.py
CHANGED
|
@@ -45,6 +45,11 @@ def get_autocomplete_model():
|
|
| 45 |
return _instance
|
| 46 |
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
# ─── Cache key helper ─────────────────────────────────────────────────────────
|
| 49 |
def _context_key(context: str) -> str:
|
| 50 |
"""Use last 5 words for cache key — preserves enough context for GPT-2 awareness."""
|
|
@@ -100,6 +105,10 @@ class HybridAutoComplete:
|
|
| 100 |
data = pickle.load(f)
|
| 101 |
self.unigrams = data["unigrams"]
|
| 102 |
self.bigrams = data["bigrams"]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
logger.info(
|
| 104 |
f"Bigram model loaded: {len(self.unigrams)} unigrams, "
|
| 105 |
f"{len(self.bigrams)} bigram contexts"
|
|
@@ -108,6 +117,7 @@ class HybridAutoComplete:
|
|
| 108 |
logger.error(f"Failed to load bigram model: {e}")
|
| 109 |
self.unigrams = {}
|
| 110 |
self.bigrams = {}
|
|
|
|
| 111 |
|
| 112 |
def _load_gpt2(self):
|
| 113 |
"""Load GPT-2 model with OOM fallback."""
|
|
@@ -195,12 +205,9 @@ class HybridAutoComplete:
|
|
| 195 |
continue
|
| 196 |
candidates.append((w, c))
|
| 197 |
|
| 198 |
-
# Fallback to
|
| 199 |
if not candidates:
|
| 200 |
-
|
| 201 |
-
if len(w) < 2:
|
| 202 |
-
continue
|
| 203 |
-
candidates.append((w, c))
|
| 204 |
|
| 205 |
if not candidates:
|
| 206 |
return []
|
|
|
|
| 45 |
return _instance
|
| 46 |
|
| 47 |
|
| 48 |
+
def is_loaded() -> bool:
|
| 49 |
+
"""Check if the autocomplete model is loaded (without triggering lazy load)."""
|
| 50 |
+
return _instance is not None and _instance.is_ready()
|
| 51 |
+
|
| 52 |
+
|
| 53 |
# ─── Cache key helper ─────────────────────────────────────────────────────────
|
| 54 |
def _context_key(context: str) -> str:
|
| 55 |
"""Use last 5 words for cache key — preserves enough context for GPT-2 awareness."""
|
|
|
|
| 105 |
data = pickle.load(f)
|
| 106 |
self.unigrams = data["unigrams"]
|
| 107 |
self.bigrams = data["bigrams"]
|
| 108 |
+
self._top_unigrams = sorted(
|
| 109 |
+
[(w, c) for w, c in self.unigrams.items() if len(w) >= 2],
|
| 110 |
+
key=lambda x: x[1], reverse=True
|
| 111 |
+
)[:200]
|
| 112 |
logger.info(
|
| 113 |
f"Bigram model loaded: {len(self.unigrams)} unigrams, "
|
| 114 |
f"{len(self.bigrams)} bigram contexts"
|
|
|
|
| 117 |
logger.error(f"Failed to load bigram model: {e}")
|
| 118 |
self.unigrams = {}
|
| 119 |
self.bigrams = {}
|
| 120 |
+
self._top_unigrams = []
|
| 121 |
|
| 122 |
def _load_gpt2(self):
|
| 123 |
"""Load GPT-2 model with OOM fallback."""
|
|
|
|
| 205 |
continue
|
| 206 |
candidates.append((w, c))
|
| 207 |
|
| 208 |
+
# Fallback to precomputed top unigrams if no bigram matches
|
| 209 |
if not candidates:
|
| 210 |
+
candidates = list(self._top_unigrams)
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
if not candidates:
|
| 213 |
return []
|
src/nlp/correction_patch.py
CHANGED
|
@@ -88,9 +88,11 @@ class PatchSet:
|
|
| 88 |
|
| 89 |
def __init__(self):
|
| 90 |
self.patches: list = []
|
|
|
|
| 91 |
|
| 92 |
def add(self, patch: CorrectionPatch):
|
| 93 |
self.patches.append(patch)
|
|
|
|
| 94 |
|
| 95 |
def resolve_overlaps(self) -> list:
|
| 96 |
"""
|
|
@@ -206,4 +208,6 @@ class PatchSet:
|
|
| 206 |
|
| 207 |
def to_list(self) -> list:
|
| 208 |
"""Serialize resolved patches for API response."""
|
| 209 |
-
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
def __init__(self):
|
| 90 |
self.patches: list = []
|
| 91 |
+
self._resolved_cache = None
|
| 92 |
|
| 93 |
def add(self, patch: CorrectionPatch):
|
| 94 |
self.patches.append(patch)
|
| 95 |
+
self._resolved_cache = None
|
| 96 |
|
| 97 |
def resolve_overlaps(self) -> list:
|
| 98 |
"""
|
|
|
|
| 208 |
|
| 209 |
def to_list(self) -> list:
|
| 210 |
"""Serialize resolved patches for API response."""
|
| 211 |
+
if self._resolved_cache is None:
|
| 212 |
+
self._resolved_cache = self.resolve_overlaps()
|
| 213 |
+
return [p.to_dict() for p in self._resolved_cache]
|
src/nlp/dialect/dialect_service.py
CHANGED
|
@@ -9,12 +9,14 @@ blocking server startup.
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
import logging
|
|
|
|
| 12 |
import torch
|
| 13 |
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
| 14 |
|
| 15 |
logger = logging.getLogger(__name__)
|
| 16 |
|
| 17 |
_instance = None
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
class DialectConverter:
|
|
@@ -26,41 +28,46 @@ class DialectConverter:
|
|
| 26 |
MAX_OUTPUT_LENGTH = 128
|
| 27 |
|
| 28 |
def __init__(self):
|
| 29 |
-
self.device = "cpu"
|
|
|
|
| 30 |
logger.info(f"[DIALECT] Loading model from '{self.REPO_ID}'...")
|
| 31 |
|
| 32 |
self.tokenizer = AutoTokenizer.from_pretrained(self.REPO_ID)
|
| 33 |
self.model = AutoModelForSeq2SeqLM.from_pretrained(
|
| 34 |
-
self.REPO_ID, torch_dtype=
|
| 35 |
).to(self.device)
|
| 36 |
self.model.eval()
|
| 37 |
|
| 38 |
-
logger.info("[DIALECT] Model loaded successfully (
|
| 39 |
|
| 40 |
def convert(self, dialect_text: str, num_beams: int = 4) -> str:
|
| 41 |
"""Convert a single dialect sentence to MSA."""
|
| 42 |
if not dialect_text or not dialect_text.strip():
|
| 43 |
return dialect_text
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
def is_ready(self) -> bool:
|
| 66 |
"""Check if the model is loaded and ready."""
|
|
@@ -71,7 +78,9 @@ def get_dialect_model() -> DialectConverter:
|
|
| 71 |
"""Get or create the singleton DialectConverter instance."""
|
| 72 |
global _instance
|
| 73 |
if _instance is None:
|
| 74 |
-
|
|
|
|
|
|
|
| 75 |
return _instance
|
| 76 |
|
| 77 |
|
|
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
import logging
|
| 12 |
+
import threading
|
| 13 |
import torch
|
| 14 |
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
| 15 |
|
| 16 |
logger = logging.getLogger(__name__)
|
| 17 |
|
| 18 |
_instance = None
|
| 19 |
+
_lock = threading.Lock()
|
| 20 |
|
| 21 |
|
| 22 |
class DialectConverter:
|
|
|
|
| 28 |
MAX_OUTPUT_LENGTH = 128
|
| 29 |
|
| 30 |
def __init__(self):
|
| 31 |
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 32 |
+
_dtype = torch.float16 if self.device == "cuda" else torch.float32
|
| 33 |
logger.info(f"[DIALECT] Loading model from '{self.REPO_ID}'...")
|
| 34 |
|
| 35 |
self.tokenizer = AutoTokenizer.from_pretrained(self.REPO_ID)
|
| 36 |
self.model = AutoModelForSeq2SeqLM.from_pretrained(
|
| 37 |
+
self.REPO_ID, torch_dtype=_dtype
|
| 38 |
).to(self.device)
|
| 39 |
self.model.eval()
|
| 40 |
|
| 41 |
+
logger.info(f"[DIALECT] Model loaded successfully ({_dtype}).")
|
| 42 |
|
| 43 |
def convert(self, dialect_text: str, num_beams: int = 4) -> str:
|
| 44 |
"""Convert a single dialect sentence to MSA."""
|
| 45 |
if not dialect_text or not dialect_text.strip():
|
| 46 |
return dialect_text
|
| 47 |
|
| 48 |
+
try:
|
| 49 |
+
input_text = self.PREFIX + dialect_text.strip()
|
| 50 |
+
inputs = self.tokenizer(
|
| 51 |
+
input_text,
|
| 52 |
+
return_tensors="pt",
|
| 53 |
+
max_length=self.MAX_INPUT_LENGTH,
|
| 54 |
+
truncation=True,
|
| 55 |
+
).to(self.device)
|
| 56 |
+
|
| 57 |
+
with torch.no_grad():
|
| 58 |
+
outputs = self.model.generate(
|
| 59 |
+
**inputs,
|
| 60 |
+
max_length=self.MAX_OUTPUT_LENGTH,
|
| 61 |
+
num_beams=num_beams,
|
| 62 |
+
early_stopping=True,
|
| 63 |
+
no_repeat_ngram_size=3,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
result = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 67 |
+
return result
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logger.warning(f"[DIALECT] Conversion failed: {e}")
|
| 70 |
+
return dialect_text
|
| 71 |
|
| 72 |
def is_ready(self) -> bool:
|
| 73 |
"""Check if the model is loaded and ready."""
|
|
|
|
| 78 |
"""Get or create the singleton DialectConverter instance."""
|
| 79 |
global _instance
|
| 80 |
if _instance is None:
|
| 81 |
+
with _lock:
|
| 82 |
+
if _instance is None:
|
| 83 |
+
_instance = DialectConverter()
|
| 84 |
return _instance
|
| 85 |
|
| 86 |
|
src/nlp/grammar/grammar_rules.py
CHANGED
|
@@ -77,31 +77,8 @@ class ArabicGrammarGuard:
|
|
| 77 |
return " ".join(corrected_tokens)
|
| 78 |
|
| 79 |
def smart_asmaa_khamsa_fix(self, text):
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
corrected_tokens = []
|
| 83 |
-
verb_seen = False
|
| 84 |
-
|
| 85 |
-
for i, token_info in enumerate(disambig_tokens):
|
| 86 |
-
word = tokens[i]
|
| 87 |
-
|
| 88 |
-
pos_tag = token_info.analyses[0].analysis.get('pos', 'unknown') if token_info.analyses else 'unknown'
|
| 89 |
-
|
| 90 |
-
if pos_tag == 'verb':
|
| 91 |
-
verb_seen = True
|
| 92 |
-
corrected_tokens.append(word)
|
| 93 |
-
continue
|
| 94 |
-
|
| 95 |
-
is_asmaa = any(word.startswith(root) or word.startswith('أ' + root[1:]) for root in self.asmaa_khamsa_roots if len(root)>1)
|
| 96 |
-
|
| 97 |
-
if is_asmaa and len(word) >= 3:
|
| 98 |
-
if verb_seen:
|
| 99 |
-
word = word.replace('ا', 'و').replace('ي', 'و')
|
| 100 |
-
verb_seen = False
|
| 101 |
-
|
| 102 |
-
corrected_tokens.append(word)
|
| 103 |
-
|
| 104 |
-
return " ".join(corrected_tokens)
|
| 105 |
|
| 106 |
def _apply_jazm_to_verb(self, word, token_info):
|
| 107 |
# 1. Handle Af'al Khamsa using camel_tools analysis
|
|
@@ -196,12 +173,16 @@ class ArabicGrammarGuard:
|
|
| 196 |
|
| 197 |
if i > 0:
|
| 198 |
prev_word = tokens[i-1]
|
| 199 |
-
if prev_word in nasb_particles
|
| 200 |
is_nasb_context = True
|
| 201 |
-
if prev_word in jazm_particles
|
| 202 |
is_jazm_context = True
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
-
is_present_tense = word.startswith('ي') or word.startswith('ت') or word.startswith('ن') or word.startswith('أ')
|
| 205 |
if (pos_tag == 'verb' or is_present_tense) and (is_nasb_context or is_jazm_context):
|
| 206 |
if is_jazm_context:
|
| 207 |
word = self._apply_jazm_to_verb(word, token_info)
|
|
@@ -223,9 +204,14 @@ class ArabicGrammarGuard:
|
|
| 223 |
# When a feminine noun is followed by a masculine adjective, add ة
|
| 224 |
# e.g. السيارة جميل → السيارة جميلة
|
| 225 |
words = text.split()
|
|
|
|
| 226 |
for i in range(len(words) - 1):
|
| 227 |
noun = words[i]
|
| 228 |
adj = words[i + 1]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
is_fem_noun = (noun in KNOWN_FEMININE_NOUNS or
|
| 230 |
(noun.endswith('ة') and len(noun) >= 3) or
|
| 231 |
(noun.startswith('ال') and noun.endswith('ة')))
|
|
@@ -250,6 +236,9 @@ class ArabicGrammarGuard:
|
|
| 250 |
'النيران', 'نيران', 'الألوان', 'ألوان', 'البلدان', 'بلدان',
|
| 251 |
'الأوطان', 'أوطان', 'الأبدان', 'أبدان', 'الأركان', 'أركان',
|
| 252 |
'الفرسان', 'فرسان', 'الغزلان', 'غزلان', 'القضبان', 'قضبان',
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
|
| 255 |
def fix_prepositions_advanced(self, text):
|
|
@@ -321,8 +310,8 @@ class ArabicGrammarGuard:
|
|
| 321 |
if word == 'ذو': return 'ذا'
|
| 322 |
if word == 'فو': return 'فا'
|
| 323 |
if word == 'حمو': return 'حما'
|
| 324 |
-
if word.endswith('ون'): return word[:-2] + 'ين'
|
| 325 |
-
if word.endswith('ان'): return word[:-2] + 'ين'
|
| 326 |
elif target_case == 'n': # Marfoo'
|
| 327 |
if word in ('أبا', 'أبي'): return 'أبو'
|
| 328 |
if word in ('أخا', 'أخي'): return 'أخو'
|
|
@@ -481,30 +470,18 @@ class ArabicGrammarGuard:
|
|
| 481 |
# FIX-08: Expanded feminine plurals
|
| 482 |
'المهندسات', 'مهندسات', 'الطبيبات', 'طبيبات',
|
| 483 |
'اللاعبات', 'لاعبات', 'الممثلات', 'ممثلات',
|
| 484 |
-
'الشركات', 'شركات', 'الجامعات', 'جامعات',
|
| 485 |
-
'المدارس', 'مدارس', 'المستشفيات', 'مستشفيات',
|
| 486 |
-
'الحكومات', 'حكومات', 'المنظمات', 'منظمات',
|
| 487 |
-
'الطائرات', 'طائرات', 'السيارات', 'سيارات',
|
| 488 |
}
|
| 489 |
|
| 490 |
-
if
|
|
|
|
|
|
|
| 491 |
is_plural_masc = True
|
| 492 |
elif noun_word in KNOWN_PLURALS_FEM:
|
| 493 |
is_plural_fem = True
|
| 494 |
-
elif noun_word.endswith('ون') or noun_word.endswith('ين'):
|
| 495 |
-
#
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
elif noun_word.endswith('ات') and len(noun_word) >= 5:
|
| 499 |
-
is_plural_fem = True
|
| 500 |
-
# FIX-08: Broken plural heuristic — common patterns
|
| 501 |
-
elif noun_num == 'p':
|
| 502 |
-
# Trust POS tagger when it says plural AND word is long enough
|
| 503 |
-
if len(noun_word) >= 4:
|
| 504 |
-
if noun_gen == 'f':
|
| 505 |
-
is_plural_fem = True
|
| 506 |
-
else:
|
| 507 |
-
is_plural_masc = True
|
| 508 |
|
| 509 |
is_singular_fem = False
|
| 510 |
if not is_plural_masc and not is_plural_fem:
|
|
@@ -574,7 +551,8 @@ class ArabicGrammarGuard:
|
|
| 574 |
return word
|
| 575 |
|
| 576 |
# إن وأخواتها
|
| 577 |
-
|
|
|
|
| 578 |
lambda m: f"{m.group(1)} {_add_hamza(m.group(2)).replace('و', 'ا')}", text)
|
| 579 |
|
| 580 |
# الأفعال المتعدية (Object position)
|
|
@@ -608,7 +586,7 @@ class ArabicGrammarGuard:
|
|
| 608 |
corrected_tokens = list(tokens)
|
| 609 |
|
| 610 |
# Lookahead for 2nd person context
|
| 611 |
-
has_2nd_person_context =
|
| 612 |
|
| 613 |
in_cond = False
|
| 614 |
verbs_jazmed = 0
|
|
@@ -634,10 +612,6 @@ class ArabicGrammarGuard:
|
|
| 634 |
# Apply jazm using the comprehensive camel_tools helper
|
| 635 |
word = self._apply_jazm_to_verb(word, token_info)
|
| 636 |
|
| 637 |
-
# Fix pronoun mismatch if 2nd person context exists
|
| 638 |
-
if has_2nd_person_context and word.startswith('ي') and (word.endswith('وا') or word.endswith('ا') or word.endswith('ي')):
|
| 639 |
-
word = 'ت' + word[1:]
|
| 640 |
-
|
| 641 |
corrected_tokens[i] = word
|
| 642 |
# Increment jazmed verbs counter (handles both فعل الشرط and جواب الشرط)
|
| 643 |
verbs_jazmed += 1
|
|
@@ -866,8 +840,9 @@ class ArabicGrammarGuard:
|
|
| 866 |
}
|
| 867 |
words = text.split()
|
| 868 |
for i, w in enumerate(words):
|
| 869 |
-
|
| 870 |
-
|
|
|
|
| 871 |
return ' '.join(words)
|
| 872 |
|
| 873 |
def fix_initial_hamza(self, text):
|
|
@@ -920,16 +895,27 @@ class ArabicGrammarGuard:
|
|
| 920 |
_PRONOUN_SUFFIXES = {'ه', 'ها', 'ك', 'كم', 'كن', 'هم', 'هن', 'ني', 'نا'}
|
| 921 |
words = text.split()
|
| 922 |
for i, w in enumerate(words):
|
| 923 |
-
|
| 924 |
-
|
|
|
|
| 925 |
continue
|
| 926 |
# إنّ/أنّ: kasra at sentence start, fathah mid-sentence
|
| 927 |
_is_sent_start = (i == 0) or (words[i-1][-1] in '.؟!؛' if words[i-1] else False)
|
| 928 |
-
if _is_sent_start and
|
| 929 |
-
words[i] = _INNA_SENTENCE_INITIAL[w]
|
| 930 |
continue
|
| 931 |
-
if not _is_sent_start and
|
| 932 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
continue
|
| 934 |
for stem, fixed in _HAMZA_STEMS.items():
|
| 935 |
if w.startswith(stem) and len(w) > len(stem):
|
|
|
|
| 77 |
return " ".join(corrected_tokens)
|
| 78 |
|
| 79 |
def smart_asmaa_khamsa_fix(self, text):
|
| 80 |
+
# Bug 1.1: Disabled blind replacement of ا and ي with و as it corrupts object position
|
| 81 |
+
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
def _apply_jazm_to_verb(self, word, token_info):
|
| 84 |
# 1. Handle Af'al Khamsa using camel_tools analysis
|
|
|
|
| 173 |
|
| 174 |
if i > 0:
|
| 175 |
prev_word = tokens[i-1]
|
| 176 |
+
if prev_word in nasb_particles:
|
| 177 |
is_nasb_context = True
|
| 178 |
+
if prev_word in jazm_particles:
|
| 179 |
is_jazm_context = True
|
| 180 |
+
# Lam Al-Ta'leel / Lam Al-Amr logic was flawed (triggering for ANY word starting with ل)
|
| 181 |
+
# Ensure it only applies to actual verbs starting with 'لي' or 'لت' etc.
|
| 182 |
+
if (word.startswith('ل') or word.startswith('ول') or word.startswith('فل')) and len(word) >= 4 and word[1] in ['ي', 'ت', 'ن', 'أ']:
|
| 183 |
+
is_nasb_context = True
|
| 184 |
|
| 185 |
+
is_present_tense = word.startswith('ي') or word.startswith('ت') or word.startswith('ن') or word.startswith('أ') or word.startswith('لي') or word.startswith('لت') or word.startswith('ولي') or word.startswith('فلي')
|
| 186 |
if (pos_tag == 'verb' or is_present_tense) and (is_nasb_context or is_jazm_context):
|
| 187 |
if is_jazm_context:
|
| 188 |
word = self._apply_jazm_to_verb(word, token_info)
|
|
|
|
| 204 |
# When a feminine noun is followed by a masculine adjective, add ة
|
| 205 |
# e.g. السيارة جميل → السيارة جميلة
|
| 206 |
words = text.split()
|
| 207 |
+
KNOWN_MASC_TA_MARBUTA = {'خليفة', 'أسامة', 'حمزة', 'طلحة', 'معاوية', 'عبيدة', 'قضاة', 'دعاة', 'رماة', 'حماة'}
|
| 208 |
for i in range(len(words) - 1):
|
| 209 |
noun = words[i]
|
| 210 |
adj = words[i + 1]
|
| 211 |
+
# Strip prefixes like 'ال' to check root
|
| 212 |
+
clean_noun = noun[2:] if noun.startswith('ال') else noun
|
| 213 |
+
if clean_noun in KNOWN_MASC_TA_MARBUTA:
|
| 214 |
+
continue
|
| 215 |
is_fem_noun = (noun in KNOWN_FEMININE_NOUNS or
|
| 216 |
(noun.endswith('ة') and len(noun) >= 3) or
|
| 217 |
(noun.startswith('ال') and noun.endswith('ة')))
|
|
|
|
| 236 |
'النيران', 'نيران', 'الألوان', 'ألوان', 'البلدان', 'بلدان',
|
| 237 |
'الأوطان', 'أوطان', 'الأبدان', 'أبدان', 'الأركان', 'أركان',
|
| 238 |
'الفرسان', 'فرسان', 'الغزلان', 'غزلان', 'القضبان', 'قضبان',
|
| 239 |
+
'فرعون', 'قانون', 'القانون', 'كانون', 'قارون', 'طاعون',
|
| 240 |
+
'عربون', 'هارون', 'زيدون', 'معجون', 'مجنون', 'زيتون', 'صابون',
|
| 241 |
+
'الفرعون', 'قوانين'
|
| 242 |
}
|
| 243 |
|
| 244 |
def fix_prepositions_advanced(self, text):
|
|
|
|
| 310 |
if word == 'ذو': return 'ذا'
|
| 311 |
if word == 'فو': return 'فا'
|
| 312 |
if word == 'حمو': return 'حما'
|
| 313 |
+
if word.endswith('ون') and word not in ('قانون', 'فرعون', 'كانون', 'معجون', 'طاعون', 'مجنون'): return word[:-2] + 'ين'
|
| 314 |
+
if word.endswith('ان') and word not in ('امتحان', 'إنسان', 'ميدان', 'سلطان', 'شيطان'): return word[:-2] + 'ين'
|
| 315 |
elif target_case == 'n': # Marfoo'
|
| 316 |
if word in ('أبا', 'أبي'): return 'أبو'
|
| 317 |
if word in ('أخا', 'أخي'): return 'أخو'
|
|
|
|
| 470 |
# FIX-08: Expanded feminine plurals
|
| 471 |
'المهندسات', 'مهندسات', 'الطبيبات', 'طبيبات',
|
| 472 |
'اللاعبات', 'لاعبات', 'الممثلات', 'ممثلات',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
}
|
| 474 |
|
| 475 |
+
if noun_num == 'd':
|
| 476 |
+
pass # Dual noun, skip plural logic to prevent corruption
|
| 477 |
+
elif noun_word in KNOWN_PLURALS_MASC:
|
| 478 |
is_plural_masc = True
|
| 479 |
elif noun_word in KNOWN_PLURALS_FEM:
|
| 480 |
is_plural_fem = True
|
| 481 |
+
elif (noun_word.endswith('ون') or (noun_word.endswith('ين') and noun_num == 'p')) and len(noun_word) >= 5:
|
| 482 |
+
# Bug 1.14: Check noun_num == 'p' to prevent duals from triggering plural logic
|
| 483 |
+
is_plural_masc = True
|
| 484 |
+
# Bug 1.2: Disabled POS tagger heuristic for broken plurals and generic 'ات' ending because it forces 'ن' on non-human plurals
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
is_singular_fem = False
|
| 487 |
if not is_plural_masc and not is_plural_fem:
|
|
|
|
| 551 |
return word
|
| 552 |
|
| 553 |
# إن وأخواتها
|
| 554 |
+
# Bug 1.13: removed كان from this regex because it caused كان أخوك -> كان أخاك
|
| 555 |
+
text = re.sub(r'\b(إن|أن|كأن|لكن|لعل|ليت|ان)\s+(أبوك|ابوك|أخوك|اخوك|ذو|فوك)\b',
|
| 556 |
lambda m: f"{m.group(1)} {_add_hamza(m.group(2)).replace('و', 'ا')}", text)
|
| 557 |
|
| 558 |
# الأفعال المتعدية (Object position)
|
|
|
|
| 586 |
corrected_tokens = list(tokens)
|
| 587 |
|
| 588 |
# Lookahead for 2nd person context
|
| 589 |
+
has_2nd_person_context = False
|
| 590 |
|
| 591 |
in_cond = False
|
| 592 |
verbs_jazmed = 0
|
|
|
|
| 612 |
# Apply jazm using the comprehensive camel_tools helper
|
| 613 |
word = self._apply_jazm_to_verb(word, token_info)
|
| 614 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
corrected_tokens[i] = word
|
| 616 |
# Increment jazmed verbs counter (handles both فعل الشرط and جواب الشرط)
|
| 617 |
verbs_jazmed += 1
|
|
|
|
| 840 |
}
|
| 841 |
words = text.split()
|
| 842 |
for i, w in enumerate(words):
|
| 843 |
+
clean_w = w.rstrip('.,،؛;:!؟?()[]{}«»"\'…')
|
| 844 |
+
if clean_w in _ALWAYS_TANWEEN:
|
| 845 |
+
words[i] = _ALWAYS_TANWEEN[clean_w] + w[len(clean_w):]
|
| 846 |
return ' '.join(words)
|
| 847 |
|
| 848 |
def fix_initial_hamza(self, text):
|
|
|
|
| 895 |
_PRONOUN_SUFFIXES = {'ه', 'ها', 'ك', 'كم', 'كن', 'هم', 'هن', 'ني', 'نا'}
|
| 896 |
words = text.split()
|
| 897 |
for i, w in enumerate(words):
|
| 898 |
+
clean_w = w.rstrip('.,،؛;:!؟?()[]{}«»"\'…')
|
| 899 |
+
if clean_w in _HAMZA_FIXES:
|
| 900 |
+
words[i] = _HAMZA_FIXES[clean_w] + w[len(clean_w):]
|
| 901 |
continue
|
| 902 |
# إنّ/أنّ: kasra at sentence start, fathah mid-sentence
|
| 903 |
_is_sent_start = (i == 0) or (words[i-1][-1] in '.؟!؛' if words[i-1] else False)
|
| 904 |
+
if _is_sent_start and clean_w in _INNA_SENTENCE_INITIAL:
|
| 905 |
+
words[i] = _INNA_SENTENCE_INITIAL[clean_w] + w[len(clean_w):]
|
| 906 |
continue
|
| 907 |
+
if not _is_sent_start and clean_w in _ANNA_MID_SENTENCE:
|
| 908 |
+
if clean_w == 'ان':
|
| 909 |
+
# Bug 1.5: Added تقول and all variants to Qawl check
|
| 910 |
+
if i > 0 and words[i-1].rstrip('.,،؛;:!؟?()[]{}«»"\'…') in ('قال', 'قالت', 'يقول', 'يقولون', 'قلت', 'قلنا', 'تقول', 'يقولوا'):
|
| 911 |
+
words[i] = 'إن' + w[len(clean_w):]
|
| 912 |
+
else:
|
| 913 |
+
pass # leave it alone to avoid breaking conditional
|
| 914 |
+
else:
|
| 915 |
+
if i > 0 and words[i-1].rstrip('.,،؛;:!؟?()[]{}«»"\'…') in ('قال', 'قالت', 'يقول', 'يقولون', 'قلت', 'قلنا', 'تقول', 'يقولوا'):
|
| 916 |
+
words[i] = _INNA_SENTENCE_INITIAL[clean_w] + w[len(clean_w):]
|
| 917 |
+
else:
|
| 918 |
+
words[i] = _ANNA_MID_SENTENCE[clean_w] + w[len(clean_w):]
|
| 919 |
continue
|
| 920 |
for stem, fixed in _HAMZA_STEMS.items():
|
| 921 |
if w.startswith(stem) and len(w) > len(stem):
|
src/nlp/grammar/grammar_service.py
CHANGED
|
@@ -10,12 +10,14 @@ Model + rules loaded on first request and kept in memory.
|
|
| 10 |
|
| 11 |
import logging
|
| 12 |
import time
|
|
|
|
| 13 |
|
| 14 |
logger = logging.getLogger(__name__)
|
| 15 |
|
| 16 |
# ── Lazy-loaded singletons ──
|
| 17 |
_grammar_checker = None
|
| 18 |
_load_error = None
|
|
|
|
| 19 |
|
| 20 |
GRADIO_SPACE = "mohammedahmedezz2004/bayan_arabic_grammarly_correction"
|
| 21 |
|
|
@@ -47,19 +49,20 @@ class GrammarChecker:
|
|
| 47 |
if len(orig_words) == len(corr_words):
|
| 48 |
result = []
|
| 49 |
for o_w, c_w in zip(orig_words, corr_words):
|
| 50 |
-
|
| 51 |
-
for ch in o_w
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
| 63 |
return " ".join(result)
|
| 64 |
|
| 65 |
# Global prefix/suffix if lengths differ
|
|
@@ -139,78 +142,82 @@ def get_grammar_model():
|
|
| 139 |
if _grammar_checker is not None:
|
| 140 |
return _grammar_checker
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
try:
|
| 146 |
-
t0 = time.time()
|
| 147 |
-
logger.info("Loading Grammar model (lazy init)...")
|
| 148 |
-
|
| 149 |
-
# 1. Initialize Gradio Client — with retry for rate limiting / sleeping Spaces
|
| 150 |
-
from gradio_client import Client
|
| 151 |
-
client = None
|
| 152 |
-
max_retries = 3
|
| 153 |
-
last_err = None
|
| 154 |
-
|
| 155 |
-
for attempt in range(1, max_retries + 1):
|
| 156 |
-
try:
|
| 157 |
-
logger.info(f"Connecting to Gradio Space: {GRADIO_SPACE} (attempt {attempt}/{max_retries})")
|
| 158 |
-
client = Client(GRADIO_SPACE)
|
| 159 |
-
logger.info("Gradio Client connected")
|
| 160 |
-
break
|
| 161 |
-
except Exception as conn_err:
|
| 162 |
-
last_err = conn_err
|
| 163 |
-
err_msg = str(conn_err).lower()
|
| 164 |
-
is_retryable = any(kw in err_msg for kw in [
|
| 165 |
-
'too many requests', 'rate limit', '429',
|
| 166 |
-
'timeout', 'connection', 'sleeping'
|
| 167 |
-
])
|
| 168 |
-
if is_retryable and attempt < max_retries:
|
| 169 |
-
wait = 2 ** attempt # 2s, 4s, 8s
|
| 170 |
-
logger.warning(
|
| 171 |
-
f"Gradio connection attempt {attempt} failed ({conn_err}). "
|
| 172 |
-
f"Retrying in {wait}s..."
|
| 173 |
-
)
|
| 174 |
-
time.sleep(wait)
|
| 175 |
-
else:
|
| 176 |
-
raise # Not retryable or last attempt — bubble up
|
| 177 |
-
|
| 178 |
-
if client is None:
|
| 179 |
-
raise RuntimeError(f"Gradio connection failed after {max_retries} attempts: {last_err}")
|
| 180 |
-
|
| 181 |
-
# 2. Initialize rule-based post-processor (camel-tools)
|
| 182 |
-
logger.info("Loading ArabicGrammarGuard (camel-tools MLE disambiguator)...")
|
| 183 |
-
from nlp.grammar.grammar_rules import ArabicGrammarGuard
|
| 184 |
-
rules = ArabicGrammarGuard()
|
| 185 |
-
logger.info("ArabicGrammarGuard loaded")
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
|
| 215 |
|
| 216 |
def is_loaded() -> bool:
|
|
|
|
| 10 |
|
| 11 |
import logging
|
| 12 |
import time
|
| 13 |
+
import threading
|
| 14 |
|
| 15 |
logger = logging.getLogger(__name__)
|
| 16 |
|
| 17 |
# ── Lazy-loaded singletons ──
|
| 18 |
_grammar_checker = None
|
| 19 |
_load_error = None
|
| 20 |
+
_lock = threading.Lock()
|
| 21 |
|
| 22 |
GRADIO_SPACE = "mohammedahmedezz2004/bayan_arabic_grammarly_correction"
|
| 23 |
|
|
|
|
| 49 |
if len(orig_words) == len(corr_words):
|
| 50 |
result = []
|
| 51 |
for o_w, c_w in zip(orig_words, corr_words):
|
| 52 |
+
c_has_punct = any(ch in PUNCT_CHARS for ch in c_w)
|
| 53 |
+
o_has_punct = any(ch in PUNCT_CHARS for ch in o_w)
|
| 54 |
+
if o_has_punct and not c_has_punct:
|
| 55 |
+
prefix = ""
|
| 56 |
+
for ch in o_w:
|
| 57 |
+
if ch in PUNCT_CHARS: prefix += ch
|
| 58 |
+
else: break
|
| 59 |
+
suffix = ""
|
| 60 |
+
for ch in reversed(o_w):
|
| 61 |
+
if ch in PUNCT_CHARS: suffix = ch + suffix
|
| 62 |
+
else: break
|
| 63 |
+
result.append(prefix + c_w + suffix)
|
| 64 |
+
else:
|
| 65 |
+
result.append(c_w)
|
| 66 |
return " ".join(result)
|
| 67 |
|
| 68 |
# Global prefix/suffix if lengths differ
|
|
|
|
| 142 |
if _grammar_checker is not None:
|
| 143 |
return _grammar_checker
|
| 144 |
|
| 145 |
+
with _lock:
|
| 146 |
+
if _grammar_checker is not None:
|
| 147 |
+
return _grammar_checker
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
+
if _load_error is not None:
|
| 150 |
+
raise RuntimeError(f"Grammar model previously failed to load: {_load_error}")
|
| 151 |
|
| 152 |
+
try:
|
| 153 |
+
t0 = time.time()
|
| 154 |
+
logger.info("Loading Grammar model (lazy init)...")
|
| 155 |
+
|
| 156 |
+
# 1. Initialize Gradio Client — with retry for rate limiting / sleeping Spaces
|
| 157 |
+
from gradio_client import Client
|
| 158 |
+
client = None
|
| 159 |
+
max_retries = 3
|
| 160 |
+
last_err = None
|
| 161 |
+
|
| 162 |
+
for attempt in range(1, max_retries + 1):
|
| 163 |
+
try:
|
| 164 |
+
logger.info(f"Connecting to Gradio Space: {GRADIO_SPACE} (attempt {attempt}/{max_retries})")
|
| 165 |
+
client = Client(GRADIO_SPACE)
|
| 166 |
+
logger.info("Gradio Client connected")
|
| 167 |
+
break
|
| 168 |
+
except Exception as conn_err:
|
| 169 |
+
last_err = conn_err
|
| 170 |
+
err_msg = str(conn_err).lower()
|
| 171 |
+
is_retryable = any(kw in err_msg for kw in [
|
| 172 |
+
'too many requests', 'rate limit', '429',
|
| 173 |
+
'timeout', 'connection', 'sleeping'
|
| 174 |
+
])
|
| 175 |
+
if is_retryable and attempt < max_retries:
|
| 176 |
+
wait = 2 ** attempt # 2s, 4s, 8s
|
| 177 |
+
logger.warning(
|
| 178 |
+
f"Gradio connection attempt {attempt} failed ({conn_err}). "
|
| 179 |
+
f"Retrying in {wait}s..."
|
| 180 |
+
)
|
| 181 |
+
time.sleep(wait)
|
| 182 |
+
else:
|
| 183 |
+
raise # Not retryable or last attempt — bubble up
|
| 184 |
+
|
| 185 |
+
if client is None:
|
| 186 |
+
raise RuntimeError(f"Gradio connection failed after {max_retries} attempts: {last_err}")
|
| 187 |
+
|
| 188 |
+
# 2. Initialize rule-based post-processor (camel-tools)
|
| 189 |
+
logger.info("Loading ArabicGrammarGuard (camel-tools MLE disambiguator)...")
|
| 190 |
+
from nlp.grammar.grammar_rules import ArabicGrammarGuard
|
| 191 |
+
rules = ArabicGrammarGuard()
|
| 192 |
+
logger.info("ArabicGrammarGuard loaded")
|
| 193 |
+
|
| 194 |
+
# 3. Create GrammarChecker instance
|
| 195 |
+
_grammar_checker = GrammarChecker(client, rules)
|
| 196 |
+
|
| 197 |
+
elapsed = time.time() - t0
|
| 198 |
+
logger.info(f"Grammar model ready in {elapsed:.1f}s")
|
| 199 |
+
return _grammar_checker
|
| 200 |
|
| 201 |
+
except Exception as e:
|
| 202 |
+
import traceback
|
| 203 |
+
error_msg = str(e)
|
| 204 |
+
logger.error(f"Failed to load grammar model: {e}")
|
| 205 |
+
logger.error(traceback.format_exc())
|
| 206 |
+
|
| 207 |
+
# Transient errors (rate limiting, network) should NOT be cached —
|
| 208 |
+
# allow retry on next request
|
| 209 |
+
transient_keywords = ['Too many requests', 'rate limit', 'timeout',
|
| 210 |
+
'ConnectionError', 'ConnectTimeout', 'ReadTimeout',
|
| 211 |
+
'429', 'sleeping']
|
| 212 |
+
is_transient = any(kw.lower() in error_msg.lower() for kw in transient_keywords)
|
| 213 |
+
|
| 214 |
+
if is_transient:
|
| 215 |
+
logger.warning(f"Grammar load error is TRANSIENT — will retry on next request: {error_msg}")
|
| 216 |
+
# Do NOT set _load_error — next call will retry
|
| 217 |
+
else:
|
| 218 |
+
_load_error = error_msg # Cache permanent failures only
|
| 219 |
+
|
| 220 |
+
raise RuntimeError(f"Grammar model load failed: {e}")
|
| 221 |
|
| 222 |
|
| 223 |
def is_loaded() -> bool:
|