youssefreda9 commited on
Commit
ee5e504
·
1 Parent(s): 1d4ba83

Fix 30 NLP edge cases in Grammar, Spelling, and Punctuation (Phase 10 results and Extension UI improvements)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. 1.png +3 -0
  2. add_divider.py +19 -0
  3. add_extension_theme_toggle.py +124 -0
  4. bug_test_report.md +402 -0
  5. check_dividers.py +5 -0
  6. check_fp.py +6 -0
  7. debug.py +19 -0
  8. extension/IMPLEMENTATION_CHANGELOG.md +0 -151
  9. extension/PLAN_apply_contextmenu_reanalysis.md +0 -262
  10. extension/assets/icons/fab_logo.png +3 -0
  11. extension/content-inline.css +553 -184
  12. extension/content-inline.js +191 -117
  13. extension/content.js +0 -11
  14. extension/manifest.json +10 -0
  15. extension/popup.css +371 -124
  16. extension/popup.html +127 -82
  17. extension/popup.js +330 -126
  18. extension/shared/bayan-state.js +0 -127
  19. extension/shared/css/tokens.css +182 -0
  20. extension/shared/vendor/docx.umd.js +0 -0
  21. extension/shared/vendor/html2pdf.bundle.min.js +0 -0
  22. extension/shared/vendor/mammoth.browser.min.js +0 -0
  23. extension/sidepanel/sidepanel.css +466 -221
  24. extension/sidepanel/sidepanel.html +149 -120
  25. extension/sidepanel/sidepanel.js +358 -279
  26. extension/tests/api_response_tc2.json +0 -41
  27. extension/tests/debug_offsets.ps1 +0 -14
  28. extension/tests/test_api_real.ps1 +0 -80
  29. extension/tests/test_e2e_real.html +0 -319
  30. extension/tests/test_inline.html +0 -78
  31. extension/tests/test_patches.html +0 -282
  32. extension/tests/test_patches.js +0 -253
  33. extension/tests/test_patches.ps1 +0 -172
  34. extension_divider.py +29 -0
  35. fix_dividers.py +19 -0
  36. fix_listener.py +28 -0
  37. fix_sp_theme.py +21 -0
  38. fix_zindex.py +12 -0
  39. inject.py +13 -0
  40. inject_ext.py +13 -0
  41. inject_logos.py +13 -0
  42. src/app.py +53 -28
  43. src/favicon.png +3 -0
  44. src/index.html +14 -5
  45. src/js/format.js +2 -1
  46. src/nlp/autocomplete/autocomplete_service.py +12 -5
  47. src/nlp/correction_patch.py +5 -1
  48. src/nlp/dialect/dialect_service.py +32 -23
  49. src/nlp/grammar/grammar_rules.py +49 -63
  50. src/nlp/grammar/grammar_service.py +89 -82
1.png ADDED

Git LFS Details

  • SHA256: ac95bbea5577ea3ec66e96a64311220b40201ed0e17e1a084aea51f1d2b16336
  • Pointer size: 131 Bytes
  • Size of remote file: 695 kB
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

  • SHA256: ac95bbea5577ea3ec66e96a64311220b40201ed0e17e1a084aea51f1d2b16336
  • Pointer size: 131 Bytes
  • Size of remote file: 695 kB
extension/content-inline.css CHANGED
@@ -7,25 +7,25 @@
7
  /* ── Error highlights (contenteditable + overlay) ── */
8
 
9
  .bayan-il-spelling {
10
- background: rgba(239, 68, 68, 0.12) !important;
11
- border-bottom: 2px solid #ef4444 !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(239, 68, 68, 0.25) !important;
18
  }
19
 
20
  .bayan-il-grammar {
21
- background: rgba(245, 158, 11, 0.12) !important;
22
- border-bottom: 2px solid #f59e0b !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(245, 158, 11, 0.25) !important;
29
  }
30
 
31
  .bayan-il-punctuation {
@@ -49,7 +49,7 @@
49
  display: flex;
50
  align-items: center;
51
  justify-content: center;
52
- background: #1a1a24 !important;
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: #6366f1 !important;
72
- box-shadow: 0 2px 14px rgba(99, 102, 241, 0.3) !important;
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 #1a1a24 !important;
93
  color: white !important;
94
- background: #6b6b80 !important;
95
  }
96
 
97
  .bayan-il-badge--clean {
98
- background: #22c55e !important;
99
  }
100
 
101
  .bayan-il-badge--errors {
102
- background: #ef4444 !important;
103
  animation: bayan-il-pulse 600ms ease-out !important;
104
  }
105
 
106
  .bayan-il-badge--analyzing {
107
- background: #6366f1 !important;
108
  animation: bayan-il-spin 1s linear infinite !important;
109
  }
110
 
111
  .bayan-il-badge--paused {
112
- background: #6b6b80 !important;
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: #1a1a24 !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(99, 102, 241, 0.1) !important;
139
  overflow: hidden;
140
- font-family: 'Segoe UI', Tahoma, Arial, sans-serif !important;
141
  font-size: 13px !important;
142
- color: #f0f0f5 !important;
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: #22222e !important;
159
  border-bottom: 1px solid #2d2d3d !important;
160
  }
161
 
@@ -168,12 +168,12 @@
168
  }
169
 
170
  .bayan-il-badge-spelling {
171
- background: rgba(239, 68, 68, 0.15) !important;
172
- color: #ef4444 !important;
173
  }
174
  .bayan-il-badge-grammar {
175
- background: rgba(245, 158, 11, 0.15) !important;
176
- color: #f59e0b !important;
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: #6b6b80 !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: #f0f0f5 !important;
194
  }
195
 
196
  .bayan-il-tooltip-body {
@@ -203,18 +203,18 @@
203
  }
204
 
205
  .bayan-il-tooltip-original {
206
- color: #ef4444 !important;
207
  text-decoration: line-through !important;
208
- text-decoration-color: rgba(239, 68, 68, 0.4) !important;
209
  }
210
 
211
  .bayan-il-tooltip-arrow {
212
- color: #6b6b80 !important;
213
  font-size: 12px !important;
214
  }
215
 
216
  .bayan-il-tooltip-correction {
217
- color: #22c55e !important;
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: #22222e !important;
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, #6366f1, #4f46e5) !important;
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(99, 102, 241, 0.3) !important;
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: #9898ad !important;
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: #2a2a38 !important;
260
- color: #f0f0f5 !important;
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: #6366f1 !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(99, 102, 241, 0.1) !important;
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
- position: fixed !important;
345
- inset: 0 !important;
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
- top: 50% !important;
363
- left: 50% !important;
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: #1a1a24 !important;
369
- border: 1px solid #3a3a4d !important;
370
- border-radius: 14px !important;
371
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(99, 102, 241, 0.1) !important;
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: #f0f0f5 !important;
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: translate(-50%, -50%) scale(1) !important;
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, #1a1a24 0%, rgba(99, 102, 241, 0.08) 100%) !important;
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, #818cf8, #6366f1) !important;
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: #6b6b80 !important;
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: #f0f0f5 !important;
429
  background: rgba(255, 255, 255, 0.08) !important;
430
  }
431
 
432
- /* ── Modal Body (scrollable) ── */
433
 
434
- .bayan-il-modal-body {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- /* ── Score Section ── */
446
 
447
- .bayan-il-modal-score {
 
 
 
 
 
 
 
 
 
448
  display: flex !important;
 
449
  align-items: center !important;
450
- gap: 14px !important;
451
- padding: 14px !important;
452
- margin-bottom: 14px !important;
453
- background: #22222e !important;
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: 80px !important;
461
- height: 80px !important;
 
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 circle:last-child {
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: 22px !important;
482
- font-weight: 800 !important;
483
- color: #818cf8 !important;
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: #9898ad !important;
495
- margin-bottom: 8px !important;
496
  }
497
 
498
- .bayan-il-modal-counts {
 
 
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: #6b6b80 !important;
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: #ef4444 !important; }
517
- .bayan-il-modal-count--grammar strong { color: #f59e0b !important; }
518
  .bayan-il-modal-count--punctuation strong { color: #6BC98A !important; }
519
 
520
- /* ── Suggestions Header ── */
 
 
 
 
 
 
 
521
 
522
  .bayan-il-modal-sugg-header {
523
  display: flex !important;
@@ -526,15 +550,12 @@
526
  margin-bottom: 10px !important;
527
  }
528
 
529
- .bayan-il-modal-sugg-title {
530
- font-size: 12px !important;
531
- font-weight: 600 !important;
532
- color: #9898ad !important;
533
- }
534
 
535
- .bayan-il-modal-sugg-count {
536
- font-size: 11px !important;
537
- color: #6b6b80 !important;
 
538
  }
539
 
540
  /* ── Suggestion Cards ── */
@@ -543,29 +564,29 @@
543
  display: flex !important;
544
  flex-direction: column !important;
545
  gap: 8px !important;
546
- margin-bottom: 14px !important;
 
547
  }
548
 
 
 
 
 
549
  .bayan-il-modal-card {
550
  padding: 10px 12px !important;
551
- background: #1a1a24 !important;
552
- border: 1px solid #2d2d3d !important;
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: #3a3a4d !important;
561
- background: #22222e !important;
562
- transform: translateX(-2px) !important;
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(239, 68, 68, 0.15) !important;
580
- color: #ef4444 !important;
581
  }
582
 
583
  .bayan-il-modal-badge--grammar {
584
- background: rgba(245, 158, 11, 0.15) !important;
585
- color: #f59e0b !important;
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(239, 68, 68, 0.4) !important;
605
- color: #ef4444 !important;
606
  opacity: 0.75 !important;
607
  }
608
 
609
  .bayan-il-modal-card-arrow {
610
- color: #6b6b80 !important;
611
  font-size: 12px !important;
612
  }
613
 
614
  .bayan-il-modal-card-fix {
615
- color: #22c55e !important;
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: #9898ad !important;
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: #6366f1 !important;
639
- color: #f0f0f5 !important;
640
- background: rgba(99, 102, 241, 0.1) !important;
641
  }
642
 
643
  .bayan-il-modal-alt-chip--main {
644
- background: #6366f1 !important;
645
- border-color: #6366f1 !important;
646
  color: white !important;
647
  }
648
 
649
  .bayan-il-modal-alt-chip--main:hover {
650
- background: #4f46e5 !important;
651
  color: white !important;
652
  }
653
 
654
  .bayan-il-modal-alt-chip--keep {
655
  border-style: dashed !important;
656
- color: #6b6b80 !important;
657
  opacity: 0.8 !important;
658
  }
659
 
660
  .bayan-il-modal-alt-chip--keep:hover {
661
  opacity: 1 !important;
662
- border-color: #6b6b80 !important;
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, #6366f1, #4f46e5) !important;
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(99, 102, 241, 0.3) !important;
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(99, 102, 241, 0.45) !important;
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: #6b6b80 !important;
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: #ffffff !important;
721
- border-color: #e5e7eb !important;
722
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(99, 102, 241, 0.08) !important;
723
- color: #1f2937 !important;
724
  }
725
 
726
- .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-header {
727
- background: linear-gradient(135deg, #ffffff 0%, rgba(99, 102, 241, 0.05) 100%) !important;
728
- border-bottom-color: #e5e7eb !important;
729
  }
730
 
731
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-close {
732
- color: #9ca3af !important;
733
  }
734
 
735
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-close:hover {
736
- color: #1f2937 !important;
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: #d1d5db !important;
 
 
 
 
742
  }
743
 
744
- .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-score {
745
- background: #f9fafb !important;
746
- border-color: #e5e7eb !important;
747
  }
748
 
749
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-score-value {
750
- color: #6366f1 !important;
751
  }
752
 
753
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-score-hint {
754
- color: #6b7280 !important;
755
  }
756
 
757
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-count {
758
- color: #9ca3af !important;
759
  }
760
 
761
- .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-sugg-title {
762
- color: #6b7280 !important;
 
763
  }
764
 
765
- .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-sugg-count {
766
- color: #9ca3af !important;
 
 
 
 
767
  }
768
 
769
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card {
770
- background: #ffffff !important;
771
- border-color: #e5e7eb !important;
772
  }
773
 
774
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card:hover {
775
- background: #f9fafb !important;
776
- border-color: #d1d5db !important;
 
777
  }
778
 
779
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card-original {
780
- color: #dc2626 !important;
781
  }
782
 
783
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card-arrow {
784
- color: #9ca3af !important;
785
  }
786
 
787
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-card-fix {
788
- color: #16a34a !important;
789
  }
790
 
791
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip {
792
- border-color: #d1d5db !important;
793
- color: #6b7280 !important;
794
  background: transparent !important;
795
  }
796
 
797
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip:hover {
798
- border-color: #6366f1 !important;
799
- color: #4f46e5 !important;
800
- background: rgba(99, 102, 241, 0.06) !important;
801
  }
802
 
803
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip--main {
804
- background: #6366f1 !important;
805
- border-color: #6366f1 !important;
806
  color: white !important;
807
  }
808
 
809
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip--keep {
810
- color: #9ca3af !important;
811
- border-color: #d1d5db !important;
812
  }
813
 
814
  .bayan-il-modal-panel[data-bayan-theme="light"] .bayan-il-modal-alt-chip--keep:hover {
815
- border-color: #9ca3af !important;
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: #9ca3af !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-tooltip';
424
  tooltip.dir = 'rtl';
425
 
 
 
 
 
426
  safeHTML(tooltip, `
427
- <div class="bayan-il-tooltip-header">
428
- <span class="bayan-il-tooltip-badge bayan-il-badge-${type}">${typeLabels[type] || type}</span>
429
- <button class="bayan-il-tooltip-close" title="إغلاق">✕</button>
430
- </div>
431
- <div class="bayan-il-tooltip-body">
432
- <span class="bayan-il-tooltip-original">${esc(original)}</span>
433
- <span class="bayan-il-tooltip-arrow">←</span>
434
- <span class="bayan-il-tooltip-correction">${correction ? esc(correction) : '<s style="opacity:0.5">حذف</s>'}</span>
435
- </div>
436
- <div class="bayan-il-tooltip-actions">
437
- <button class="bayan-il-tooltip-apply" data-action="apply">تطبيق</button>
438
- <button class="bayan-il-tooltip-ignore" data-action="ignore">تجاهل</button>
439
- </div>
 
440
  `);
441
 
442
  document.body.appendChild(tooltip);
@@ -468,13 +519,11 @@
468
  hideTooltip();
469
  });
470
 
471
- tooltip.querySelector('[data-action="ignore"]').addEventListener('click', () => {
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
- <svg width="18" height="18" viewBox="0 0 100 100" fill="none">
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 (modalBackdrop?.contains(a)) return;
999
  if (document.querySelector('.bayan-il-tooltip')) return;
1000
- detachField();
 
 
 
1001
  }, 300);
1002
  }, true);
1003
 
1004
  window.addEventListener('scroll', () => {
1005
- if (activeField && floatingBtn) positionFab(activeField);
1006
- if (overlayContainer && activeField) {
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
- if (activeField && floatingBtn) positionFab(activeField);
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 (modalBackdrop) return;
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-header">
1108
  <div class="bayan-il-modal-brand">
1109
- <svg width="22" height="22" viewBox="0 0 100 100" fill="none">
1110
- <circle cx="50" cy="50" r="46" fill="url(#bmGrad)" />
1111
- <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)" />
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
- <div class="bayan-il-modal-body">
1125
- <div class="bayan-il-modal-score">
1126
- <div class="bayan-il-modal-score-ring">
1127
- <svg viewBox="0 0 160 160">
1128
- <circle cx="80" cy="80" r="70" fill="none" stroke="#2d2d3d" stroke-width="8" />
1129
- <circle cx="80" cy="80" r="70" fill="none" stroke="#6366f1" stroke-width="8"
1130
- stroke-dasharray="${SCORE_CIRCUMFERENCE}" stroke-dashoffset="${SCORE_CIRCUMFERENCE}" stroke-linecap="round"
1131
- id="bayan-modal-score-circle" transform="rotate(-90 80 80)" />
 
1132
  </svg>
1133
- <span class="bayan-il-modal-score-value" id="bayan-modal-score-value">--</span>
1134
  </div>
1135
- <div class="bayan-il-modal-score-meta">
1136
- <span class="bayan-il-modal-score-hint" id="bayan-modal-score-hint"></span>
1137
- <div class="bayan-il-modal-counts">
1138
- <span class="bayan-il-modal-count bayan-il-modal-count--spelling">إملائي: <strong id="bayan-modal-count-spelling">٠</strong></span>
1139
- <span class="bayan-il-modal-count bayan-il-modal-count--grammar">نحوي: <strong id="bayan-modal-count-grammar">٠</strong></span>
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
- <div class="bayan-il-modal-sugg-header">
1145
- <span class="bayan-il-modal-sugg-title">الاقتراحات</span>
1146
- <span class="bayan-il-modal-sugg-count" id="bayan-modal-sugg-count"></span>
 
 
 
 
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' && modalBackdrop?.classList.contains('bayan-il-modal-backdrop--visible')) {
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
- modalBackdrop.classList.add('bayan-il-modal-backdrop--visible');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- function updateModalThemeColors() {
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 = SCORE_CIRCUMFERENCE - (SCORE_CIRCUMFERENCE * score) / 100;
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) valueEl.textContent = String(score);
 
 
 
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 = `${toArabicNum(suggestions.length)} اقتراح`;
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 (modalBackdrop) modalBackdrop.setAttribute('data-bayan-theme', currentBayanTheme);
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 (modalBackdrop) modalBackdrop.setAttribute('data-bayan-theme', currentBayanTheme);
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: #6366f1;
9
- --bayan-primary-light: #818cf8;
10
- --bayan-primary-dark: #4f46e5;
11
- --bayan-primary-glow: rgba(99, 102, 241, 0.25);
12
-
13
- --bayan-bg: #0f0f14;
14
- --bayan-surface: #1a1a24;
15
- --bayan-surface-hover: #22222e;
16
- --bayan-surface-active: #2a2a38;
17
- --bayan-border: #2d2d3d;
18
- --bayan-border-light: #3a3a4d;
19
-
20
- --bayan-text: #f0f0f5;
21
- --bayan-text-secondary: #9898ad;
22
- --bayan-text-muted: #6b6b80;
23
-
24
- --bayan-success: #22c55e;
25
- --bayan-warning: #f59e0b;
26
- --bayan-error: #ef4444;
27
-
28
- --bayan-spelling: #ef4444;
29
- --bayan-grammar: #f59e0b;
30
- --bayan-punctuation: #6BC98A;
31
 
32
  --bayan-radius: 10px;
33
  --bayan-radius-sm: 6px;
34
  --bayan-radius-lg: 14px;
35
 
36
- --bayan-font: 'Segoe UI', 'SF Pro', Tahoma, Arial, sans-serif;
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(--bayan-text);
60
- background: var(--bayan-bg);
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(--bayan-border);
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(--bayan-surface) 0%, var(--bayan-bg) 100%);
86
- border-bottom: 1px solid var(--bayan-border);
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(--bayan-primary-light), var(--bayan-primary));
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(34, 197, 94, 0.5);
127
  }
128
  .bayan-status-dot.offline {
129
  background: var(--bayan-error);
130
- box-shadow: 0 0 6px rgba(239, 68, 68, 0.4);
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(--bayan-border);
140
- background: var(--bayan-surface);
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 4px;
150
  border: none;
151
  background: transparent;
152
  color: var(--bayan-text-muted);
153
  font-family: var(--bayan-font-arabic);
154
- font-size: 12px;
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(--bayan-primary);
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(--bayan-text);
178
- background: var(--bayan-surface-hover);
179
  }
180
 
181
  .bayan-tab.active {
182
- color: var(--bayan-primary-light);
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(--bayan-text-secondary);
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(--bayan-border);
232
  border-radius: var(--bayan-radius);
233
- background: var(--bayan-surface);
234
- color: var(--bayan-text);
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(--bayan-primary);
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(--bayan-primary), var(--bayan-primary-dark));
293
  color: white;
294
- box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
295
  }
296
  .bayan-btn-primary:hover {
297
- box-shadow: 0 4px 16px rgba(99, 102, 241, 0.45);
298
  transform: translateY(-1px);
299
  }
300
  .bayan-btn-primary:active {
301
  transform: translateY(0);
302
- box-shadow: 0 1px 4px rgba(99, 102, 241, 0.2);
303
  }
304
 
305
  .bayan-btn-ghost {
306
  background: transparent;
307
- color: var(--bayan-text-secondary);
308
- border: 1px solid var(--bayan-border);
309
  }
310
  .bayan-btn-ghost:hover {
311
- background: var(--bayan-surface-hover);
312
- color: var(--bayan-text);
313
  }
314
 
315
  .bayan-btn-sm {
316
  padding: 5px 12px;
317
  font-size: 11px;
318
- background: var(--bayan-primary);
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(--bayan-surface-hover);
342
- color: var(--bayan-text);
343
  }
344
 
345
  /* ══════════════════════════════════════════════
@@ -347,21 +354,30 @@ body::-webkit-scrollbar-thumb {
347
  ══════════════════════════════════════════════ */
348
  .bayan-score-section {
349
  display: flex;
 
350
  align-items: center;
351
- gap: 14px;
352
- padding: 12px;
353
  margin-bottom: 14px;
354
- background: var(--bayan-surface);
355
- border: 1px solid var(--bayan-border);
356
  border-radius: var(--bayan-radius);
357
  animation: fadeSlideIn 300ms ease-out;
358
  }
359
 
 
 
 
 
 
 
 
360
  .bayan-score-ring {
361
  position: relative;
362
- width: 80px;
363
- height: 80px;
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: 20px;
382
  font-weight: 700;
383
- color: var(--bayan-primary-light);
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(--bayan-text-secondary);
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(--bayan-text-secondary);
436
  }
437
 
438
  .bayan-result-text {
439
  padding: 12px 14px;
440
- background: var(--bayan-surface);
441
- border: 1px solid var(--bayan-border);
442
  border-radius: var(--bayan-radius);
443
  font-size: 14px;
444
  line-height: 2;
445
- color: var(--bayan-text);
446
  min-height: 60px;
447
  word-wrap: break-word;
448
  }
449
 
450
  /* Error highlights in result */
451
  .bayan-spelling-error {
452
- background: rgba(239, 68, 68, 0.15);
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(239, 68, 68, 0.25);
461
  }
462
 
463
  .bayan-grammar-error {
464
- background: rgba(245, 158, 11, 0.15);
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(245, 158, 11, 0.25);
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(--bayan-text-secondary);
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(--bayan-surface);
530
- border: 1px solid var(--bayan-border);
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(--bayan-primary);
537
- background: var(--bayan-surface-hover);
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(239, 68, 68, 0.15);
552
  color: var(--bayan-spelling);
553
  }
554
  .bayan-badge-grammar {
555
- background: rgba(245, 158, 11, 0.15);
556
  color: var(--bayan-grammar);
557
  }
558
  .bayan-badge-punctuation {
559
- background: rgba(107, 201, 138, 0.15);
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(--bayan-border);
596
  border-radius: 20px;
597
  background: transparent;
598
- color: var(--bayan-text-secondary);
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(--bayan-primary);
606
- color: var(--bayan-text);
607
  background: var(--bayan-surface-active);
608
  }
609
 
610
  .bayan-alt-chip--main {
611
- background: var(--bayan-primary);
612
- border-color: var(--bayan-primary);
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(--bayan-border);
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(--bayan-surface-hover);
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(--bayan-text-secondary);
669
  cursor: pointer;
670
  }
671
 
672
  .bayan-radio input[type="radio"] {
673
- accent-color: var(--bayan-primary);
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(--bayan-border);
699
- border-top-color: var(--bayan-primary);
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(--bayan-text-secondary);
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(--bayan-surface);
723
- border: 1px solid var(--bayan-border);
724
  border-radius: 20px;
725
- color: var(--bayan-text);
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 Theme Variables */
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(--bayan-surface-hover);
797
- color: var(--bayan-text-secondary);
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(--bayan-primary);
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
- <div class="bayan-header-brand">
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
- </div>
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
- <label class="bayan-label" for="input-text">أدخل النص العربي</label>
 
 
 
 
 
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="80" height="80">
93
- <circle cx="80" cy="80" r="70" fill="none" stroke="var(--bayan-border)" stroke-width="8"/>
94
- <circle cx="80" cy="80" r="70" fill="none" stroke="var(--bayan-primary)" stroke-width="8"
 
 
 
 
 
 
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
- <div class="bayan-score-meta">
101
- <span class="bayan-score-hint" id="score-hint">ابدأ الكتابة لرؤية تقييمك</span>
102
- <div class="bayan-counts" id="error-counts">
103
- <span class="bayan-count bayan-count-spelling"><span id="count-spelling">٠</span> إملائي</span>
104
- <span class="bayan-count bayan-count-grammar"><span id="count-grammar">٠</span> نحوي</span>
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-char-count">٠</span> حرف
 
 
 
 
 
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
- <button class="bayan-btn-icon" id="btn-copy-summary" type="button" title="نسخ">
179
- <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>
180
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
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">حوّل العامية إلى الفصحى</label>
 
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
- <div class="bayan-result is-hidden" id="quran-result-section">
243
- <div class="bayan-result-header">
244
- <span class="bayan-result-title">نتيجة التدقيق</span>
 
 
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
- <div class="bayan-result-text" id="quran-text" dir="rtl"></div>
250
- <div class="bayan-summary-meta" id="quran-meta"></div>
251
- </div>
252
- </section>
253
 
254
- <!-- ══════════════════════════════════════════════ -->
255
- <!-- Autocomplete Panel (إكمال) -->
256
- <!-- ══════════════════════════════════════════════ -->
257
- <section class="bayan-panel" id="panel-autocomplete" role="tabpanel">
258
- <div class="bayan-input-group">
259
- <label class="bayan-label" for="autocomplete-input-text">اقتراحات إكمال الجملة</label>
260
- <textarea
261
- id="autocomplete-input-text"
262
- class="bayan-textarea"
263
- dir="rtl"
264
- placeholder="اكتب بداية الجملة..."
265
- rows="5"
266
- maxlength="5000"
267
- ></textarea>
268
- <div class="bayan-input-meta"><span id="autocomplete-char-count">٠</span> حرف</div>
269
- </div>
270
- <div class="bayan-actions">
271
- <button class="bayan-btn bayan-btn-primary" id="btn-autocomplete" type="button">
272
- <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>
273
- اقترح إكمالاً
274
- </button>
275
- </div>
276
- <div class="bayan-suggestions is-hidden" id="autocomplete-result-section">
277
- <div class="bayan-suggestions-header">
278
- <span class="bayan-suggestions-title">اقتراحات الإكمال</span>
 
 
 
 
 
 
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 btnCopyResult = document.getElementById('btn-copy-result');
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 summaryCharCount = document.getElementById('summary-char-count');
32
  const btnSummarize = document.getElementById('btn-summarize');
33
  const summaryResultSection = document.getElementById('summary-result-section');
34
  const summaryText = document.getElementById('summary-text');
35
- const summaryMeta = document.getElementById('summary-meta');
 
 
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', () => updateCounts(summaryInputText, summaryCharCount, null));
 
 
 
 
 
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; // already stale
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 < CONFIG.MIN_ANALYZE_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 with highlights
300
- resultSection.classList.remove('is-hidden');
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 result (MED-1: .catch() for clipboard errors)
355
  // ══════════════════════════════════════════════════════════
356
- btnCopyResult.addEventListener('click', () => {
357
- const text = resultText.textContent || '';
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
- summaryText.textContent = data.summary;
387
- summaryMeta.textContent = `النص الأصلي: ${(data.original_length || 0).toLocaleString('ar-EG')} حرف → الملخص: ${(data.summary_length || 0).toLocaleString('ar-EG')} حرف`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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', () => updateCounts(dialectInput, dialectCharCount, null));
 
 
 
 
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 quranText = document.getElementById('quran-text');
460
- const quranMeta = document.getElementById('quran-meta');
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
- quranText.textContent = data.error;
476
- quranMeta.textContent = '';
477
- } else {
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
- showToast('خطأ في الاتصال — تحقق من الإنترنت');
 
 
487
  } finally {
488
  setLoading(false);
489
  }
490
  });
491
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  btnCopyQuran.addEventListener('click', () => {
493
- navigator.clipboard.writeText(quranText.textContent || '')
 
494
  .then(() => showToast('✓ تم النسخ'))
495
  .catch(() => showToast('تعذّر النسخ'));
496
  });
 
 
 
 
 
 
 
 
 
 
497
  }
498
 
499
  // ══════════════════════════════════════════════════════════
500
- // Autocomplete suggestions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  // ══════════════════════════════════════════════════════════
502
- const acInput = document.getElementById('autocomplete-input-text');
503
- const acCharCount = document.getElementById('autocomplete-char-count');
504
- const btnAutocomplete = document.getElementById('btn-autocomplete');
505
- const acResultSection = document.getElementById('autocomplete-result-section');
506
- const acList = document.getElementById('autocomplete-list');
507
 
508
- if (acInput) {
509
- acInput.addEventListener('input', () => updateCounts(acInput, acCharCount, null));
 
 
 
510
 
511
- btnAutocomplete.addEventListener('click', async () => {
512
- const text = acInput.value;
513
- if (!text.trim() || text.trim().length < 3) { showToast('اكتب ٣ أحرف على الأقل'); return; }
514
 
515
- setLoading(true, 'جارٍ الاقتراح...');
516
- try {
517
- const data = await bayanAutocomplete(text, 5);
518
- const suggestions = data.suggestions || [];
519
- acResultSection.classList.remove('is-hidden');
520
 
521
- if (suggestions.length === 0) {
522
- acList.innerHTML = '<div class="bayan-ac-empty">لا توجد اقتراحات</div>';
523
- return;
524
- }
 
525
 
526
- acList.innerHTML = suggestions
527
- .map((s) => `<button class="bayan-alt-chip bayan-alt-chip--main bayan-ac-chip" type="button">${escapeHtml(s)}</button>`)
528
- .join('');
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
- // Phase 5: Download corrected text / summary as .txt
550
- // Buttons are injected programmatically to avoid touching popup.html.
551
- // ══════════════════════════════════════════════════════════
552
- function downloadTxt(text, filename) {
553
- if (!text) { showToast('لا يوجد نص للتنزيل'); return; }
554
- try {
555
- const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
556
- const url = URL.createObjectURL(blob);
557
- const a = document.createElement('a');
558
- a.href = url;
559
- a.download = filename;
560
- document.body.appendChild(a);
561
- a.click();
562
- a.remove();
563
- setTimeout(() => URL.revokeObjectURL(url), 1000);
564
- showToast('✓ تم تنزيل الملف');
565
- } catch (e) {
566
- console.error('[Bayan] Download error:', e);
567
- showToast('تعذّر التنزيل');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  }
569
  }
570
 
571
- const DOWNLOAD_ICON = '<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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M12 4v12m0 0l-4-4m4 4l4-4"/></svg>';
572
-
573
- function addDownloadButton(headerEl, getText, filename) {
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
- addDownloadButton(
585
- btnCopyResult ? btnCopyResult.parentElement : null,
586
- () => resultText.textContent,
587
- 'bayan-corrected.txt'
588
- );
589
- addDownloadButton(
590
- btnCopySummary ? btnCopySummary.parentElement : null,
591
- () => summaryText.textContent,
592
- 'bayan-summary.txt'
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
- updateCounts(summaryInputText, summaryCharCount, null);
 
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: #6366f1;
9
- --sp-primary-light: #818cf8;
10
- --sp-primary-dark: #4f46e5;
11
- --sp-primary-glow: rgba(99, 102, 241, 0.25);
12
 
13
- --sp-bg: #0f0f14;
14
- --sp-surface: #1a1a24;
15
- --sp-surface-hover: #22222e;
16
- --sp-surface-active: #2a2a38;
17
- --sp-border: #2d2d3d;
18
- --sp-border-light: #3a3a4d;
19
 
20
- --sp-text: #f0f0f5;
21
- --sp-text-secondary: #9898ad;
22
- --sp-text-muted: #6b6b80;
23
 
24
- --sp-success: #22c55e;
25
- --sp-warning: #f59e0b;
26
- --sp-error: #ef4444;
27
 
28
- --sp-spelling: #ef4444;
29
- --sp-grammar: #f59e0b;
30
- --sp-punctuation: #6BC98A;
31
 
32
- --sp-radius: 10px;
33
- --sp-radius-sm: 6px;
34
- --sp-radius-lg: 14px;
35
 
36
- --sp-font: 'Segoe UI', 'SF Pro', Tahoma, Arial, sans-serif;
37
- --sp-font-arabic: 'Noto Sans Arabic', 'Segoe UI', Tahoma, sans-serif;
38
 
39
- --sp-transition: 200ms cubic-bezier(0.4, 0, 0.2, 1);
40
- --sp-spring: 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
 
 
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(--sp-font-arabic);
56
  font-size: 13px;
57
  line-height: 1.6;
58
- color: var(--sp-text);
59
- background: var(--sp-bg);
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(--sp-border); border-radius: 4px; }
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(--sp-surface) 0%, rgba(99, 102, 241, 0.08) 100%);
78
- border-bottom: 1px solid var(--sp-border);
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(--sp-primary-light), #a78bfa);
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(--sp-surface);
127
- border-bottom: 1px solid var(--sp-border);
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 0;
137
  border: none;
138
  background: transparent;
139
  color: var(--sp-text-muted);
140
- font-family: var(--sp-font-arabic);
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
- .sp-tab:hover { color: var(--sp-text-secondary); }
 
148
  .sp-tab.active {
149
- color: var(--sp-primary-light);
150
- border-bottom-color: var(--sp-primary);
 
 
 
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(--sp-border);
179
  border-radius: var(--sp-radius);
180
- background: var(--sp-surface);
181
- color: var(--sp-text);
182
- font-family: var(--sp-font-arabic);
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(--sp-primary);
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(--sp-font-arabic);
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(--sp-primary), var(--sp-primary-dark));
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(--sp-surface);
251
- color: var(--sp-text-secondary);
252
- border: 1px solid var(--sp-border);
253
  }
254
  .sp-btn-ghost:hover {
255
- background: var(--sp-surface-hover);
256
- color: var(--sp-text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(--sp-surface);
268
- color: var(--sp-text-secondary);
269
  cursor: pointer;
270
  transition: all var(--sp-transition);
271
  }
272
  .sp-btn-icon:hover {
273
- background: var(--sp-surface-hover);
274
- color: var(--sp-primary-light);
275
  }
276
 
277
  /* ══════════════════════════════════════════════
278
- Score
279
  ══════════════════════════════════════════════ */
280
  .sp-score {
281
  display: flex;
 
282
  align-items: center;
283
- gap: 12px;
284
- padding: 12px;
285
  margin-bottom: 12px;
286
- background: var(--sp-surface);
287
- border: 1px solid var(--sp-border);
288
  border-radius: var(--sp-radius);
 
 
 
 
 
 
 
 
289
  }
290
 
291
  .sp-score-ring {
292
  position: relative;
293
- width: 64px;
294
- height: 64px;
295
  flex-shrink: 0;
 
296
  }
297
  .sp-score-ring svg {
298
- width: 100%;
299
- height: 100%;
300
  }
301
- .sp-score-ring circle:last-of-type {
302
- transition: stroke-dashoffset 800ms cubic-bezier(0.4, 0, 0.2, 1);
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: 18px;
312
- font-weight: 800;
313
- color: var(--sp-primary-light);
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: 11px;
324
- color: var(--sp-text-secondary);
325
- margin-bottom: 6px;
326
  }
327
 
328
  .sp-score-counts {
329
  display: flex;
330
  gap: 10px;
331
  flex-wrap: wrap;
332
- font-size: 10px;
 
 
 
 
333
  color: var(--sp-text-muted);
 
 
 
334
  }
335
- .sp-count-spelling strong { color: var(--sp-spelling); }
336
- .sp-count-grammar strong { color: var(--sp-grammar); }
337
- .sp-count-punctuation strong { color: var(--sp-punctuation); }
 
 
 
338
 
339
  /* ══════════════════════════════════════════════
340
  Results
341
  ══════════════════════════════════════════════ */
342
  .sp-result {
343
  margin-bottom: 12px;
344
- border: 1px solid var(--sp-border);
345
  border-radius: var(--sp-radius);
346
- background: var(--sp-surface);
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(--sp-text-secondary);
358
- background: var(--sp-surface-hover);
359
- border-bottom: 1px solid var(--sp-border);
 
 
 
 
 
 
360
  }
361
 
362
  .sp-result-actions {
363
  display: flex;
364
- gap: 4px;
 
365
  }
366
 
367
  .sp-result-text {
368
  padding: 10px 12px;
369
  font-size: 14px;
370
  line-height: 1.8;
371
- color: var(--sp-text);
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
- font-size: 11px;
458
- font-weight: 700;
459
- color: var(--sp-text-secondary);
460
- margin-bottom: 6px;
461
- padding: 0 2px;
 
 
 
 
 
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(--sp-surface);
488
- border: 1px solid var(--sp-border);
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(--sp-surface-hover);
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(--sp-border);
542
  border-radius: 12px;
543
- background: var(--sp-surface-hover);
544
- color: var(--sp-text);
545
- font-family: var(--sp-font-arabic);
546
  font-size: 11px;
547
  cursor: pointer;
548
  transition: all var(--sp-transition);
549
  }
550
  .bayan-alt-chip:hover {
551
- border-color: var(--sp-primary);
552
  background: var(--sp-primary-glow);
553
  }
554
  .bayan-alt-chip--main {
555
- border-color: var(--sp-primary);
556
  background: rgba(99, 102, 241, 0.1);
557
- color: var(--sp-primary-light);
558
  }
559
  .bayan-alt-chip--keep {
560
- border-color: var(--sp-border);
561
  color: var(--sp-text-muted);
562
  font-size: 10px;
563
  }
@@ -567,12 +583,176 @@ body {
567
  }
568
 
569
  /* ══════════════════════════════════════════════
570
- Radio group (summary length)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  ══════════════════════════════════════════════ */
 
 
 
 
572
  .sp-radio-group {
573
  display: flex;
574
  gap: 12px;
575
- margin-bottom: 10px;
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(--sp-text-secondary);
585
  cursor: pointer;
586
  }
587
  .sp-radio input[type="radio"] {
588
- accent-color: var(--sp-primary);
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(--sp-surface);
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(--sp-text);
621
  font-size: 13px;
622
  }
623
 
624
  .sp-spinner {
625
  width: 32px;
626
  height: 32px;
627
- border: 3px solid var(--sp-border);
628
- border-top-color: var(--sp-primary);
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(--sp-surface);
647
- border: 1px solid var(--sp-border);
648
  border-radius: 20px;
649
- color: var(--sp-text);
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(--sp-surface-hover);
708
- color: var(--sp-text-secondary);
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(--sp-primary);
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
- <div class="sp-header-brand">
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
- </div>
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
- <!-- Score ring -->
 
 
 
 
 
 
 
 
 
88
  <div class="sp-score is-hidden" id="score-section">
 
89
  <div class="sp-score-ring">
90
- <svg viewBox="0 0 160 160">
91
- <circle cx="80" cy="80" r="70" fill="none" stroke="var(--sp-border)" stroke-width="8" />
92
- <circle cx="80" cy="80" r="70" fill="none" stroke="var(--sp-primary)" stroke-width="8"
 
 
 
 
 
 
93
  stroke-dasharray="440" stroke-dashoffset="440" stroke-linecap="round"
94
- id="score-circle" transform="rotate(-90 80 80)" />
95
  </svg>
96
  <span class="sp-score-value" id="score-value">--</span>
97
  </div>
98
- <div class="sp-score-details">
99
- <span class="sp-score-hint" id="score-hint"></span>
100
- <div class="sp-score-counts">
101
- <span class="sp-count-spelling">إملائي: <strong id="count-spelling">٠</strong></span>
102
- <span class="sp-count-grammar">نحوي: <strong id="count-grammar">٠</strong></span>
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-char-count"><span id="summary-char-count">٠</span> حرف</span>
 
 
 
 
 
154
  </div>
155
  </div>
156
 
157
- <!-- Summary length options -->
158
- <div class="sp-radio-group">
159
- <label class="sp-radio"><input type="radio" name="summary-length" value="1"> قصير</label>
160
- <label class="sp-radio"><input type="radio" name="summary-length" value="2" checked> متوسط</label>
161
- <label class="sp-radio"><input type="radio" name="summary-length" value="3"> طويل</label>
 
 
 
 
 
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
- <button class="sp-btn-icon" id="btn-copy-summary" title="نسخ">
176
- <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>
177
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <button class="sp-btn-icon" id="btn-copy-dialect" title="نسخ">
211
- <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>
212
- </button>
 
 
 
 
 
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
- <div class="sp-result is-hidden" id="quran-result-section">
242
- <div class="sp-result-header">
243
- <span>نتيجة التدقيق</span>
244
- <button class="sp-btn-icon" id="btn-copy-quran" title="نسخ">
245
- <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>
246
- </button>
 
 
 
 
 
 
 
247
  </div>
248
- <div class="sp-result-text" id="quran-text" dir="rtl"></div>
249
- <div class="sp-summary-meta" id="quran-meta"></div>
250
 
251
- <!-- Verse translation (14 languages) -->
252
- <div class="sp-quran-translate is-hidden" id="quran-translate-section">
253
  <div class="sp-quran-translate-row">
254
- <span class="sp-quran-translate-label">ترجمة الآية</span>
255
- <select id="quran-lang-select" class="sp-select" dir="ltr">
 
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 class="sp-result is-hidden" id="quran-translation-result" style="margin-top:8px;">
273
- <div class="sp-result-header">
274
- <span>الترجمة</span>
275
- <div class="sp-result-actions">
276
- <button class="sp-btn-icon" id="btn-apply-quran-translation" title="تطبيق في الصفحة">
277
- <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>
278
- </button>
279
- <button class="sp-btn-icon" id="btn-copy-quran-translation" title="نسخ الترجمة">
280
- <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>
281
- </button>
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 — reuse ALL shared modules -->
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
- * Phase 5: Persistent workspace panel
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 btnCopyResult = document.getElementById('btn-copy-result');
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 summaryCharCount = document.getElementById('summary-char-count');
45
  const btnSummarize = document.getElementById('btn-summarize');
46
  const summaryResultSection = document.getElementById('summary-result-section');
47
  const summaryText = document.getElementById('summary-text');
48
- const summaryMeta = document.getElementById('summary-meta');
 
 
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
- updateCounts(summaryInputText, summaryCharCount, null);
 
 
 
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 < CONFIG.MIN_ANALYZE_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
- btnCopyResult.addEventListener('click', () => {
412
- const text = resultText.textContent || '';
413
- navigator.clipboard.writeText(text)
414
- .then(() => showToast('✓ تم نسخ النص'))
415
- .catch(() => showToast('تعذّر النسخ'));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- summaryText.textContent = data.summary;
434
- summaryMeta.textContent = `النص الأصلي: ${(data.original_length || 0).toLocaleString('ar-EG')} حرف → الملخص: ${(data.summary_length || 0).toLocaleString('ar-EG')} حرف`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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', () => updateCounts(dialectInput, dialectCharCount, null));
 
 
 
 
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 quranText = document.getElementById('quran-text');
504
- const quranMeta = document.getElementById('quran-meta');
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 quranTranslationResult = document.getElementById('quran-translation-result');
510
- const quranTranslationText = document.getElementById('quran-translation-text');
511
- const quranTranslationRef = document.getElementById('quran-translation-ref');
512
- const btnCopyQuranTranslation = document.getElementById('btn-copy-quran-translation');
513
- const btnApplyQuranTranslation = document.getElementById('btn-apply-quran-translation');
514
- let lastQuranQuery = '';
515
-
516
- // Parse the API's "(verse text) 【surah:ayah】" segment into {text, ref}.
517
- function parseQuranSegment(seg) {
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
- // Reset translation UI for the new query.
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
- quranText.textContent = data.error;
541
- quranMeta.textContent = '';
542
- } else {
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
- showToast('خطأ في الاتصال — تحقق من الإنترنت');
 
 
555
  } finally {
556
  setLoading(false);
557
  }
558
  });
559
 
560
- // Translate the verified verse into the chosen language.
561
- if (quranLangSelect) {
562
- quranLangSelect.addEventListener('change', async () => {
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
- if (btnCopyQuranTranslation) {
589
- btnCopyQuranTranslation.addEventListener('click', () => {
590
- navigator.clipboard.writeText(quranTranslationText.textContent || '')
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 bayanAutocomplete(text, 5);
632
- const suggestions = data.suggestions || [];
633
- acResultSection.classList.remove('is-hidden');
634
 
635
- if (suggestions.length === 0) {
636
- acList.innerHTML = '<div class="sp-ac-empty">لا توجد اقتراحات</div>';
637
  return;
638
  }
639
 
640
- acList.innerHTML = suggestions
641
- .map((s) => `<button class="bayan-alt-chip bayan-alt-chip--main sp-ac-chip" type="button">${escapeHtml(s)}</button>`)
642
- .join('');
643
-
644
- acList.querySelectorAll('.sp-ac-chip').forEach((chip) => {
645
- chip.addEventListener('click', () => {
646
- const needsSpace = acInput.value.length > 0 && !/\s$/.test(acInput.value);
647
- acInput.value += (needsSpace ? ' ' : '') + chip.textContent;
648
- updateCounts(acInput, acCharCount, null);
649
- acInput.focus();
650
- showToast('✓ تمت الإضافة');
651
- });
652
- });
 
 
653
  } catch (error) {
654
- console.error('[Bayan SP] Autocomplete error:', error);
655
- showToast('خطأ في الاتصال — تحقق من الإنترنت');
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
- const SP_DOWNLOAD_ICON = '<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>';
686
-
687
- function addDownloadButton(anchorBtn, getText, filename) {
688
- if (!anchorBtn || !anchorBtn.parentElement) return;
689
- const btn = document.createElement('button');
690
- btn.className = 'sp-btn-icon';
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
- addDownloadButton(btnCopyResult, () => resultText.textContent, 'bayan-corrected.txt');
699
- addDownloadButton(btnCopySummary, () => summaryText.textContent, 'bayan-summary.txt');
 
 
 
 
 
 
 
700
 
701
- // ══════════════════════════════════════════════════════════
702
- // "Apply to page" buttons for summarize / dialect / quran results.
703
- // These write the model output back into the source page field via
704
- // Change 1's relay, tagging the write with its `source` so the content
705
- // script suppresses correction re-analysis on it (Change 3).
706
- // Injected programmatically to avoid touching sidepanel.html.
707
- // ══════════════════════════════════════════════════════════
708
- const SP_APPLY_PAGE_ICON = '<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>';
 
709
 
710
- function addApplyToPageButton(anchorBtn, getText, source) {
711
- if (!anchorBtn || !anchorBtn.parentElement) return;
712
- const btn = document.createElement('button');
713
- btn.className = 'sp-btn-icon';
714
- btn.type = 'button';
715
- btn.title = 'تطبيق في الصفحة';
716
- btn.innerHTML = SP_APPLY_PAGE_ICON;
717
- btn.addEventListener('click', () => {
718
- const text = (getText() || '').trim();
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 (from background.js → storage)
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
- updateCounts(summaryInputText, summaryCharCount, null);
 
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
- updateCounts(dialectInput, dialectCharCount, null);
 
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'; // default to 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 = True # Toggleable trace logging
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
  # 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 _instance
311
- return _instance is not None and _instance.is_ready()
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} > 300 — skipping AraSpell for performance")
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 or _has_url or _has_email or _has_hashtag or _has_percent or _has_latin_word
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 = sum(len(w) + 1 for w in _oov_words[:_ow_idx])
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 = sum(len(w) + 1 for w in _oov_words[:_ow_idx])
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 = sum(len(w) + 1 for w in _oov_words[:_ow_idx])
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 = sum(len(w) + 1 for w in _oov_words[:_ow_idx])
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
- # Restore in forward order
2218
- for _sp_start, _sp_end, _sp_text in reversed(_structured_placeholders):
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
- # If original has no spaces but correction does (grammar split a name)
2358
- _has_latin = any('A' <= c <= 'Z' or 'a' <= c <= 'z' for c in orig_text)
2359
- if _has_latin and orig_text != corr_text:
 
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.error(traceback.format_exc())
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=1.0
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] Rejected bracket-unbalanced output: "
2564
- f"orig=({orig_opens},{orig_closes}), corr=({corr_opens},{corr_closes})"
 
2565
  )
2566
- # Don't mutate text — keep pre-grammar text
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.start_original, _capped_p.end_original)
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

  • SHA256: ac95bbea5577ea3ec66e96a64311220b40201ed0e17e1a084aea51f1d2b16336
  • Pointer size: 131 Bytes
  • Size of remote file: 695 kB
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="https://raw.githubusercontent.com/mohamedatef24/BAYAN/main/extension/assets/icons/icon128.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,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);">0 حرف</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,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);">٠ / ٥٬٠٠٠ حرف</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,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 originalText = getEditorText();
 
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 unigram if no bigram matches
199
  if not candidates:
200
- for w, c in self.unigrams.items():
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
- return [p.to_dict() for p in self.resolve_overlaps()]
 
 
 
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=torch.float16
35
  ).to(self.device)
36
  self.model.eval()
37
 
38
- logger.info("[DIALECT] Model loaded successfully (float16).")
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
- input_text = self.PREFIX + dialect_text.strip()
46
- inputs = self.tokenizer(
47
- input_text,
48
- return_tensors="pt",
49
- max_length=self.MAX_INPUT_LENGTH,
50
- truncation=True,
51
- ).to(self.device)
52
-
53
- with torch.no_grad():
54
- outputs = self.model.generate(
55
- **inputs,
56
- max_length=self.MAX_OUTPUT_LENGTH,
57
- num_beams=num_beams,
58
- early_stopping=True,
59
- no_repeat_ngram_size=3,
60
- )
61
-
62
- result = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
63
- return result
 
 
 
 
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
- _instance = DialectConverter()
 
 
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
- tokens = simple_word_tokenize(text)
81
- disambig_tokens = self.mle.disambiguate(tokens)
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 or word.startswith('ل'):
200
  is_nasb_context = True
201
- if prev_word in jazm_particles or word.startswith('ل') or word.startswith('ول'):
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 noun_word in KNOWN_PLURALS_MASC:
 
 
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
- # Sound masculine plural but only if 4+ chars (avoid short words)
496
- if len(noun_word) >= 5:
497
- is_plural_masc = True
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
- text = re.sub(r'\b(إن|أن|كأن|لكن|لعل|ليت|ان|كان)\s+(أبوك|ابوك|أخوك|اخوك|ذو|فوك)\b',
 
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 = any(t.endswith('كم') or t.endswith('كمو') or t.startswith('ت') for t in tokens)
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
- if w in _ALWAYS_TANWEEN:
870
- words[i] = _ALWAYS_TANWEEN[w]
 
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
- if w in _HAMZA_FIXES:
924
- words[i] = _HAMZA_FIXES[w]
 
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 w in _INNA_SENTENCE_INITIAL:
929
- words[i] = _INNA_SENTENCE_INITIAL[w]
930
  continue
931
- if not _is_sent_start and w in _ANNA_MID_SENTENCE:
932
- words[i] = _ANNA_MID_SENTENCE[w]
 
 
 
 
 
 
 
 
 
 
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
- prefix = ""
51
- for ch in o_w:
52
- if ch in PUNCT_CHARS: prefix += ch
53
- else: break
54
- suffix = ""
55
- for ch in reversed(o_w):
56
- if ch in PUNCT_CHARS: suffix = ch + suffix
57
- else: break
58
-
59
- c_base = c_w.strip('.,;:!?،؛؟!.:«»"\'()-–—…')
60
- if not c_base:
61
- c_base = c_w
62
- result.append(prefix + c_base + suffix)
 
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
- if _load_error is not None:
143
- raise RuntimeError(f"Grammar model previously failed to load: {_load_error}")
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
- # 3. Create GrammarChecker instance
188
- _grammar_checker = GrammarChecker(client, rules)
189
 
190
- elapsed = time.time() - t0
191
- logger.info(f"Grammar model ready in {elapsed:.1f}s")
192
- return _grammar_checker
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
- except Exception as e:
195
- import traceback
196
- error_msg = str(e)
197
- logger.error(f"Failed to load grammar model: {e}")
198
- logger.error(traceback.format_exc())
199
-
200
- # Transient errors (rate limiting, network) should NOT be cached —
201
- # allow retry on next request
202
- transient_keywords = ['Too many requests', 'rate limit', 'timeout',
203
- 'ConnectionError', 'ConnectTimeout', 'ReadTimeout',
204
- '429', 'sleeping']
205
- is_transient = any(kw.lower() in error_msg.lower() for kw in transient_keywords)
206
-
207
- if is_transient:
208
- logger.warning(f"Grammar load error is TRANSIENT — will retry on next request: {error_msg}")
209
- # Do NOT set _load_error — next call will retry
210
- else:
211
- _load_error = error_msg # Cache permanent failures only
212
-
213
- raise RuntimeError(f"Grammar model load failed: {e}")
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: