Mohamed Atef commited on
Commit
9784112
·
1 Parent(s): 3ab74e1

delete the extention to add the new version due to bug

Browse files
extension/_locales/ar/messages.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "extName": {
3
- "message": "بيان — مساعد الكتابة العربية",
4
- "description": "Extension name shown in Chrome"
5
- },
6
- "extDescription": {
7
- "message": "تصحيح إملائي ونحوي وترقيمي للنصوص العربية مع تلخيص ذكي",
8
- "description": "Extension description shown in Chrome Web Store"
9
- },
10
- "contextMenuCorrect": {
11
- "message": "تصحيح مع بيان",
12
- "description": "Context menu item for correcting selected text"
13
- },
14
- "contextMenuSummarize": {
15
- "message": "تلخيص مع بيان",
16
- "description": "Context menu item for summarizing selected text"
17
- }
18
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/background.js DELETED
@@ -1,194 +0,0 @@
1
- /**
2
- * Bayan — Background Service Worker (Phase 7.1 Simplified)
3
- *
4
- * Single network boundary. Owns:
5
- * 1. Context menu → side panel (Phase 5)
6
- * 2. API calls with cache + timeout + retry (Phase 7)
7
- *
8
- * Uses: BAYAN (shared/constants.js), bayanHash (shared/hash.js)
9
- * loaded via importScripts().
10
- */
11
-
12
- importScripts('shared/constants.js', 'shared/hash.js');
13
-
14
- // ── Context constants ──
15
- const ACTIONS = { CORRECT: 'correct', SUMMARIZE: 'summarize' };
16
- const CONTEXT_KEYS = ['contextAction', 'contextText', 'contextTimestamp'];
17
- const SIDE_PANEL_PATH = 'sidepanel/sidepanel.html';
18
-
19
- // ── Deduplication cache ──
20
- const _cache = new Map();
21
-
22
- function cacheGet(text) {
23
- const key = bayanHash(text);
24
- const entry = _cache.get(key);
25
- if (!entry) return null;
26
- if (Date.now() - entry.ts > BAYAN.CACHE_TTL_MS) { _cache.delete(key); return null; }
27
- if (entry.text !== text) return null;
28
- // FIX-19: LRU — move to end on access (Map maintains insertion order)
29
- _cache.delete(key);
30
- _cache.set(key, entry);
31
- return entry.data;
32
- }
33
-
34
- function cacheSet(text, data) {
35
- const key = bayanHash(text);
36
- if (_cache.size >= BAYAN.CACHE_MAX_ENTRIES) {
37
- _cache.delete(_cache.keys().next().value);
38
- }
39
- _cache.set(key, { text, data, ts: Date.now() });
40
- }
41
-
42
- // ── Storage helper ──
43
- function getStorage() {
44
- return chrome.storage?.session || chrome.storage?.local;
45
- }
46
-
47
- // ══════════════════════════════════════════════════════════
48
- // Context Menu
49
- // ══════════════════════════════════════════════════════════
50
-
51
- chrome.runtime.onInstalled.addListener(() => {
52
- chrome.contextMenus.create({
53
- id: 'bayan-correct',
54
- title: chrome.i18n.getMessage('contextMenuCorrect') || 'تصحيح مع بيان',
55
- contexts: ['selection'],
56
- });
57
- chrome.contextMenus.create({
58
- id: 'bayan-summarize',
59
- title: chrome.i18n.getMessage('contextMenuSummarize') || 'تلخيص مع بيان',
60
- contexts: ['selection'],
61
- });
62
- });
63
-
64
- chrome.contextMenus.onClicked.addListener((info, tab) => {
65
- const selectedText = info.selectionText?.trim();
66
- if (!selectedText) return;
67
-
68
- let action = null;
69
- if (info.menuItemId === 'bayan-correct') action = ACTIONS.CORRECT;
70
- if (info.menuItemId === 'bayan-summarize') action = ACTIONS.SUMMARIZE;
71
- if (!action) return;
72
-
73
- // Open side panel IMMEDIATELY (preserve user gesture)
74
- if (chrome.sidePanel) {
75
- chrome.sidePanel.open({ windowId: tab.windowId }).catch(() => {
76
- chrome.tabs.create({ url: chrome.runtime.getURL(SIDE_PANEL_PATH) });
77
- });
78
- } else {
79
- chrome.tabs.create({ url: chrome.runtime.getURL(SIDE_PANEL_PATH) });
80
- }
81
-
82
- // Store context (async)
83
- const storage = getStorage();
84
- storage.set({
85
- contextAction: action,
86
- contextText: selectedText,
87
- contextTimestamp: Date.now(),
88
- });
89
- setTimeout(() => storage.remove(CONTEXT_KEYS), 15000);
90
- });
91
-
92
- // ══════════════════════════════════════════════════════════
93
- // Message Handler
94
- // ══════════════════════════════════════════════════════════
95
-
96
- chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
97
- if (message.type === 'HEALTH_CHECK') {
98
- sendResponse({
99
- status: 'ok',
100
- version: chrome.runtime.getManifest().version,
101
- cache: _cache.size,
102
- });
103
- return true;
104
- }
105
-
106
- if (message.type === 'CLEAR_CONTEXT') {
107
- getStorage().remove(CONTEXT_KEYS);
108
- sendResponse({ status: 'cleared' });
109
- return true;
110
- }
111
-
112
- if (message.type === 'INLINE_ANALYZE') {
113
- const text = message.text;
114
- if (!text || text.length < BAYAN.MIN_TEXT_LENGTH) {
115
- sendResponse({ error: 'Text too short' });
116
- return true;
117
- }
118
- if (text.length > BAYAN.MAX_TEXT_LENGTH) {
119
- sendResponse({ error: 'Text too long' });
120
- return true;
121
- }
122
-
123
- // Cache hit → zero network
124
- const cached = cacheGet(text);
125
- if (cached) {
126
- sendResponse({ data: cached, cached: true });
127
- return true;
128
- }
129
-
130
- // Network call with retry
131
- analyzeWithRetry(text)
132
- .then((data) => {
133
- if (data) cacheSet(text, data);
134
- sendResponse({ data: data || null });
135
- })
136
- .catch((err) => {
137
- console.warn('[Bayan BG] Analysis error:', err.message);
138
- sendResponse({ error: err.message });
139
- });
140
-
141
- return true;
142
- }
143
-
144
- if (message.type === 'OPEN_SIDEPANEL') {
145
- const storage = getStorage();
146
- storage.set({
147
- contextAction: ACTIONS.CORRECT,
148
- contextText: message.text || '',
149
- contextTimestamp: Date.now(),
150
- });
151
- if (chrome.sidePanel && sender.tab) {
152
- chrome.sidePanel.open({ windowId: sender.tab.windowId }).catch(() => {
153
- chrome.tabs.create({ url: chrome.runtime.getURL(SIDE_PANEL_PATH) });
154
- });
155
- }
156
- sendResponse({ status: 'ok' });
157
- return true;
158
- }
159
-
160
- return false;
161
- });
162
-
163
- // ══════════════════════════════════════════════════════════
164
- // Network: fetch with timeout + 1 retry
165
- // ══════════════════════════════════════════════════════════
166
-
167
- async function analyzeWithRetry(text, attempt = 0) {
168
- try {
169
- return await fetchWithTimeout(text);
170
- } catch (err) {
171
- if (attempt < BAYAN.MAX_RETRIES) {
172
- return analyzeWithRetry(text, attempt + 1);
173
- }
174
- throw err;
175
- }
176
- }
177
-
178
- async function fetchWithTimeout(text) {
179
- const controller = new AbortController();
180
- const timeout = setTimeout(() => controller.abort(), BAYAN.API_TIMEOUT_MS);
181
-
182
- try {
183
- const res = await fetch(`${BAYAN.API_BASE}/api/analyze`, {
184
- method: 'POST',
185
- headers: { 'Content-Type': 'application/json' },
186
- body: JSON.stringify({ text }),
187
- signal: controller.signal,
188
- });
189
- if (!res.ok) throw new Error(`API ${res.status}`);
190
- return await res.json();
191
- } finally {
192
- clearTimeout(timeout);
193
- }
194
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/content-inline.css DELETED
@@ -1,268 +0,0 @@
1
- /* ══════════════════════════════════════════════
2
- Bayan Inline Layer — Phase 6
3
- All classes prefixed with bayan-il- to avoid conflicts.
4
- Uses !important sparingly to override page styles.
5
- ══════════════════════════════════════════════ */
6
-
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 {
32
- background: rgba(59, 130, 246, 0.12) !important;
33
- border-bottom: 2px solid #3b82f6 !important;
34
- border-radius: 2px !important;
35
- cursor: pointer !important;
36
- transition: background 150ms ease !important;
37
- }
38
- .bayan-il-punctuation:hover {
39
- background: rgba(59, 130, 246, 0.25) !important;
40
- }
41
-
42
- /* ── Floating Action Button (FAB) ── */
43
-
44
- .bayan-il-fab {
45
- position: absolute;
46
- z-index: 2147483646 !important;
47
- width: 34px;
48
- height: 34px;
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;
56
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
57
- opacity: 0;
58
- transform: scale(0.7);
59
- transition: opacity 200ms ease, transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
60
- pointer-events: none;
61
- user-select: none !important;
62
- }
63
-
64
- .bayan-il-fab--visible {
65
- opacity: 1 !important;
66
- transform: scale(1) !important;
67
- pointer-events: auto !important;
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) ── */
76
-
77
- .bayan-il-badge {
78
- position: absolute !important;
79
- top: -4px;
80
- right: -4px;
81
- min-width: 16px;
82
- height: 16px;
83
- display: flex;
84
- align-items: center;
85
- justify-content: center;
86
- font-size: 9px !important;
87
- font-weight: 700 !important;
88
- font-family: -apple-system, 'Segoe UI', sans-serif !important;
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
-
116
- @keyframes bayan-il-pulse {
117
- 0% { transform: scale(1); }
118
- 50% { transform: scale(1.3); }
119
- 100% { transform: scale(1); }
120
- }
121
-
122
- @keyframes bayan-il-spin {
123
- 0% { opacity: 0.5; }
124
- 50% { opacity: 1; }
125
- 100% { opacity: 0.5; }
126
- }
127
-
128
- /* ── Tooltip (suggestion popup) ── */
129
-
130
- .bayan-il-tooltip {
131
- position: absolute;
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;
146
- }
147
-
148
- .bayan-il-tooltip--visible {
149
- opacity: 1 !important;
150
- transform: translateY(0) !important;
151
- }
152
-
153
- .bayan-il-tooltip-header {
154
- display: flex !important;
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
-
162
- .bayan-il-tooltip-badge {
163
- display: inline-block !important;
164
- padding: 2px 8px !important;
165
- border-radius: 4px !important;
166
- font-size: 10px !important;
167
- font-weight: 700 !important;
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(59, 130, 246, 0.15) !important;
180
- color: #3b82f6 !important;
181
- }
182
-
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 {
197
- display: flex !important;
198
- align-items: center !important;
199
- gap: 8px !important;
200
- padding: 10px 12px !important;
201
- font-size: 14px !important;
202
- line-height: 1.6 !important;
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
-
221
- .bayan-il-tooltip-actions {
222
- display: flex !important;
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 {
230
- flex: 1;
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;
240
- transition: transform 150ms ease, box-shadow 150ms ease !important;
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 {
248
- padding: 5px 12px !important;
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) ── */
264
-
265
- .bayan-il-overlay {
266
- background: transparent !important;
267
- pointer-events: none !important;
268
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/content-inline.js DELETED
@@ -1,514 +0,0 @@
1
- /**
2
- * Bayan — Inline Analysis Engine (Phase 7.1 Simplified)
3
- *
4
- * Dependencies: BayanController (analysis-controller.js)
5
- * State: local variables only (no external state manager)
6
- *
7
- * Architecture:
8
- * 1. DETECTION — find editable fields
9
- * 2. ANALYSIS — debounced via BayanController → background.js
10
- * 3. OVERLAY — highlights, tooltip, FAB (never modifies user DOM content)
11
- *
12
- * Safety:
13
- * - Overlay-only rendering for ALL field types (never replaces innerHTML of user content)
14
- * - Trusted Types for Gmail/Google Sites
15
- * - Error boundary with graceful pause
16
- * - Protected sites: disable contenteditable on Gmail/Docs/Notion
17
- */
18
-
19
- (function () {
20
- 'use strict';
21
-
22
- if (window.location.protocol === 'chrome-extension:') return;
23
-
24
- if (typeof BayanController === 'undefined') {
25
- console.error('[Bayan] BayanController not loaded.');
26
- return;
27
- }
28
-
29
- // ══════════════════════════════════════════════════════════
30
- // Trusted Types
31
- // ══════════════════════════════════════════════════════════
32
- let ttPolicy = null;
33
- try {
34
- if (window.trustedTypes?.createPolicy) {
35
- ttPolicy = window.trustedTypes.createPolicy('bayan-inline-policy', {
36
- createHTML: (input) => input,
37
- });
38
- }
39
- } catch {}
40
-
41
- function safeHTML(el, html) {
42
- try {
43
- el.innerHTML = ttPolicy ? ttPolicy.createHTML(html) : html;
44
- } catch {
45
- el.textContent = '';
46
- const t = document.createElement('template');
47
- t.innerHTML = html;
48
- el.appendChild(t.content.cloneNode(true));
49
- }
50
- }
51
-
52
- // ══════════════════════════════════════════════════════════
53
- // Local state (replaces BayanState — simpler, no extra file)
54
- // ══════════════════════════════════════════════════════════
55
- const IS_PROTECTED = BayanController.isProtectedSite();
56
-
57
- let activeField = null;
58
- let lastAnalyzedText = '';
59
- let suggestions = [];
60
- let paused = false;
61
- let floatingBtn = null;
62
- let tooltip = null;
63
- let overlayContainer = null;
64
- let badgeCount = null;
65
- let observer = null;
66
-
67
- // ══════════════════════════════════════════════════════════
68
- // 1. DETECTION
69
- // ══════════════════════════════════════════════════════════
70
-
71
- function isEditableField(el) {
72
- if (!el?.tagName) return false;
73
- const tag = el.tagName.toLowerCase();
74
- if (tag === 'textarea') return true;
75
- if (tag === 'input') {
76
- const type = (el.type || '').toLowerCase();
77
- return ['text', 'search', 'url', ''].includes(type);
78
- }
79
- // Contenteditable: allow on non-protected sites
80
- if (el.isContentEditable && !IS_PROTECTED) return true;
81
- return false;
82
- }
83
-
84
- function getFieldText(field) {
85
- if (!field) return '';
86
- const tag = field.tagName.toLowerCase();
87
- if (tag === 'textarea' || tag === 'input') return field.value || '';
88
- return field.innerText || field.textContent || '';
89
- }
90
-
91
- // ══════════════════════════════════════════════════════════
92
- // 2. ANALYSIS (delegates to BayanController)
93
- // ══════════════════════════════════════════════════════════
94
-
95
- function onFieldInput() {
96
- if (paused || !activeField) return;
97
-
98
- const text = getFieldText(activeField);
99
-
100
- if (!BayanController.hasArabic(text)) {
101
- clearHighlights();
102
- updateBadge(0);
103
- return;
104
- }
105
-
106
- updateBadge(-1);
107
-
108
- BayanController.scheduleAnalysis(
109
- text,
110
- (result) => {
111
- try { onAnalysisResult(result); } catch (err) {
112
- console.warn('[Bayan] Callback error:', err.message);
113
- handleFailure('callback_error');
114
- }
115
- },
116
- () => getFieldText(activeField)
117
- );
118
- }
119
-
120
- function onAnalysisResult(data) {
121
- if (!activeField) return;
122
-
123
- if (!data) {
124
- updateBadge(0);
125
- return;
126
- }
127
-
128
- suggestions = data.suggestions || [];
129
- lastAnalyzedText = data.original || getFieldText(activeField);
130
- updateBadge(suggestions.length);
131
-
132
- if (suggestions.length > 0) {
133
- renderOverlay(activeField, lastAnalyzedText, suggestions);
134
- } else {
135
- clearHighlights();
136
- }
137
- }
138
-
139
- // ═══════════════��══════════════════════════════════════════
140
- // Error boundary
141
- // ══════════════════════════════════════════════════════════
142
-
143
- function handleFailure(reason) {
144
- paused = true;
145
- clearHighlights();
146
- BayanController.cancelAll();
147
- if (badgeCount) {
148
- badgeCount.textContent = '⏸';
149
- badgeCount.className = 'bayan-il-badge bayan-il-badge--paused';
150
- }
151
- console.warn(`[Bayan] Inline paused: ${reason}. Side Panel still works.`);
152
- }
153
-
154
- // ══════════════════════════════════════════════════════════
155
- // 3. OVERLAY — always overlay, never modify user content
156
- //
157
- // Task 10: ALL field types use overlay rendering.
158
- // This prevents formatting destruction on Medium, Slack,
159
- // WordPress, Ghost, Discourse, etc.
160
- // ══════════════════════════════════════════════════════════
161
-
162
- function renderOverlay(field, originalText, suggs) {
163
- clearHighlights();
164
- if (!field || !suggs?.length) return;
165
-
166
- try {
167
- const rect = field.getBoundingClientRect();
168
- const cs = window.getComputedStyle(field);
169
-
170
- overlayContainer = document.createElement('div');
171
- overlayContainer.className = 'bayan-il-overlay';
172
- overlayContainer.style.cssText = `position:absolute;top:${rect.top + window.scrollY}px;`
173
- + `left:${rect.left + window.scrollX}px;width:${rect.width}px;height:${rect.height}px;`
174
- + `font-family:${cs.fontFamily};font-size:${cs.fontSize};line-height:${cs.lineHeight};`
175
- + `padding:${cs.padding};border:${cs.border};border-color:transparent;`
176
- + `direction:${cs.direction};text-align:${cs.textAlign};overflow:hidden;`
177
- + `pointer-events:none;z-index:2147483645;box-sizing:border-box;`
178
- + `white-space:pre-wrap;word-wrap:break-word;color:transparent;`;
179
-
180
- const sorted = [...suggs].sort((a, b) => a.start - b.start);
181
- let html = '';
182
- let pos = 0;
183
-
184
- sorted.forEach((s) => {
185
- if (pos < s.start) html += esc(originalText.slice(pos, s.start));
186
- html += `<span class="${errCls(s.type)} bayan-il-error bayan-il-overlay-mark" `
187
- + `data-bayan-sid="${s.id || ''}" data-bayan-original="${esc(s.original)}" `
188
- + `data-bayan-correction="${esc(s.correction)}" data-bayan-type="${s.type}" `
189
- + `data-bayan-start="${s.start}" data-bayan-end="${s.end}" `
190
- + `style="pointer-events:auto;cursor:pointer;">`
191
- + `${esc(originalText.slice(s.start, s.end))}</span>`;
192
- pos = s.end;
193
- });
194
- if (pos < originalText.length) html += esc(originalText.slice(pos));
195
-
196
- safeHTML(overlayContainer, html);
197
- overlayContainer.scrollTop = field.scrollTop;
198
- overlayContainer.scrollLeft = field.scrollLeft;
199
- document.body.appendChild(overlayContainer);
200
-
201
- overlayContainer.addEventListener('mousedown', (e) => e.preventDefault());
202
-
203
- field.addEventListener('scroll', syncOverlay);
204
-
205
- overlayContainer.querySelectorAll('.bayan-il-overlay-mark').forEach((mark) => {
206
- mark.addEventListener('click', (e) => {
207
- e.stopPropagation();
208
- showTooltip(mark, activeField);
209
- });
210
- });
211
- } catch (err) {
212
- console.warn('[Bayan] Overlay error:', err.message);
213
- handleFailure('render_error');
214
- }
215
- }
216
-
217
- function syncOverlay() {
218
- if (overlayContainer && activeField) {
219
- overlayContainer.scrollTop = activeField.scrollTop;
220
- overlayContainer.scrollLeft = activeField.scrollLeft;
221
- }
222
- }
223
-
224
- function clearHighlights() {
225
- if (overlayContainer) { overlayContainer.remove(); overlayContainer = null; }
226
- hideTooltip();
227
- }
228
-
229
- // ══════════════════════════════════════════════════════════
230
- // Tooltip
231
- // ══════════════════════════════════════════════════════════
232
-
233
- function showTooltip(el, field) {
234
- hideTooltip();
235
-
236
- const original = el.dataset.bayanOriginal;
237
- const correction = el.dataset.bayanCorrection;
238
- const type = el.dataset.bayanType;
239
- const start = parseInt(el.dataset.bayanStart, 10);
240
- const end = parseInt(el.dataset.bayanEnd, 10);
241
- const sid = el.dataset.bayanSid;
242
-
243
- if (!original || !correction) return;
244
-
245
- const suggestion = { id: sid, original, correction, type, start, end };
246
- const typeLabels = { spelling: 'إملائي', grammar: 'نحوي', punctuation: 'ترقيم' };
247
-
248
- tooltip = document.createElement('div');
249
- tooltip.className = 'bayan-il-tooltip';
250
- tooltip.dir = 'rtl';
251
-
252
- safeHTML(tooltip, `
253
- <div class="bayan-il-tooltip-header">
254
- <span class="bayan-il-tooltip-badge bayan-il-badge-${type}">${typeLabels[type] || type}</span>
255
- <button class="bayan-il-tooltip-close" title="إغلاق">✕</button>
256
- </div>
257
- <div class="bayan-il-tooltip-body">
258
- <span class="bayan-il-tooltip-original">${esc(original)}</span>
259
- <span class="bayan-il-tooltip-arrow">←</span>
260
- <span class="bayan-il-tooltip-correction">${correction ? esc(correction) : '<s style="opacity:0.5">حذف</s>'}</span>
261
- </div>
262
- <div class="bayan-il-tooltip-actions">
263
- <button class="bayan-il-tooltip-apply" data-action="apply">تطبيق</button>
264
- <button class="bayan-il-tooltip-ignore" data-action="ignore">تجاهل</button>
265
- </div>
266
- `);
267
-
268
- document.body.appendChild(tooltip);
269
-
270
- const r = el.getBoundingClientRect();
271
- tooltip.style.top = `${r.bottom + window.scrollY + 6}px`;
272
- tooltip.style.left = `${r.left + window.scrollX}px`;
273
-
274
- requestAnimationFrame(() => {
275
- if (!tooltip) return;
276
- const tr = tooltip.getBoundingClientRect();
277
- if (tr.right > window.innerWidth) tooltip.style.left = `${window.innerWidth - tr.width - 8}px`;
278
- tooltip.classList.add('bayan-il-tooltip--visible');
279
- });
280
-
281
- tooltip.querySelector('[data-action="apply"]').addEventListener('click', () => {
282
- applyFix(field, suggestion);
283
- hideTooltip();
284
- });
285
-
286
- tooltip.querySelector('[data-action="ignore"]').addEventListener('click', () => {
287
- dismissSuggestion(suggestion);
288
- hideTooltip();
289
- });
290
-
291
- tooltip.querySelector('.bayan-il-tooltip-close').addEventListener('click', () => hideTooltip());
292
-
293
- setTimeout(() => document.addEventListener('click', outsideClick, { once: true }), 100);
294
- }
295
-
296
- function outsideClick(e) {
297
- if (tooltip && !tooltip.contains(e.target)) hideTooltip();
298
- }
299
-
300
- function hideTooltip() {
301
- if (tooltip) { tooltip.remove(); tooltip = null; }
302
- }
303
-
304
- // ══════════════════════════════════════════════════════════
305
- // Fix application (overlay-only: modify field value, re-render overlay)
306
- // ══════════════════════════════════════════════════════════
307
-
308
- function applyFix(field, suggestion) {
309
- if (!field) return;
310
-
311
- const isInput = field.tagName.toLowerCase() === 'textarea' || field.tagName.toLowerCase() === 'input';
312
- const text = isInput ? field.value : getFieldText(field);
313
- const before = text.substring(0, suggestion.start);
314
- const after = text.substring(suggestion.end);
315
- const newText = before + suggestion.correction + after;
316
-
317
- const delta = suggestion.correction.length - (suggestion.end - suggestion.start);
318
- suggestions = suggestions
319
- .filter((s) => s.id !== suggestion.id)
320
- .map((s) => s.start >= suggestion.end ? { ...s, start: s.start + delta, end: s.end + delta } : s);
321
-
322
- lastAnalyzedText = newText;
323
-
324
- if (isInput) {
325
- field.value = newText;
326
- field.dispatchEvent(new Event('input', { bubbles: true }));
327
- } else {
328
- // FIX-17: Use textContent instead of innerText for better compatibility
329
- // This preserves the DOM structure better than innerText assignment
330
- field.textContent = newText;
331
- }
332
-
333
- if (suggestions.length > 0) {
334
- renderOverlay(field, newText, suggestions);
335
- } else {
336
- clearHighlights();
337
- // FIX-21: Trigger re-analysis after applying last fix
338
- // This catches any remaining errors that were masked by the fixed one
339
- setTimeout(() => onFieldInput(), 500);
340
- }
341
- updateBadge(suggestions.length);
342
- }
343
-
344
- function dismissSuggestion(suggestion) {
345
- suggestions = suggestions.filter((s) => s.id !== suggestion.id);
346
- if (suggestions.length > 0) {
347
- renderOverlay(activeField, lastAnalyzedText, suggestions);
348
- } else {
349
- clearHighlights();
350
- }
351
- updateBadge(suggestions.length);
352
- }
353
-
354
- // ══════════════════════════════════════════════════════════
355
- // UI: FAB + Badge
356
- // ══════════════════════════════════════════════════════════
357
-
358
- function createFloatingBtn() {
359
- if (floatingBtn) return;
360
-
361
- floatingBtn = document.createElement('div');
362
- floatingBtn.className = 'bayan-il-fab';
363
- safeHTML(floatingBtn, `
364
- <svg width="18" height="18" viewBox="0 0 100 100" fill="none">
365
- <circle cx="50" cy="50" r="46" fill="url(#blGrad)" />
366
- <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)" />
367
- <circle cx="50" cy="42" r="4" fill="url(#blGrad)" />
368
- <defs>
369
- <linearGradient id="blGrad" x1="0" y1="0" x2="100" y2="100">
370
- <stop offset="0%" stop-color="#6366f1"/>
371
- <stop offset="100%" stop-color="#8b5cf6"/>
372
- </linearGradient>
373
- </defs>
374
- </svg>
375
- <span class="bayan-il-badge">0</span>
376
- `);
377
- floatingBtn.title = 'Bayan — بيان';
378
- document.body.appendChild(floatingBtn);
379
- badgeCount = floatingBtn.querySelector('.bayan-il-badge');
380
-
381
- floatingBtn.addEventListener('click', (e) => {
382
- e.stopPropagation();
383
- if (paused) {
384
- paused = false;
385
- updateBadge(0);
386
- onFieldInput();
387
- return;
388
- }
389
- if (suggestions.length > 0) {
390
- try {
391
- chrome.runtime.sendMessage({ type: 'OPEN_SIDEPANEL', text: lastAnalyzedText });
392
- } catch {}
393
- }
394
- });
395
- }
396
-
397
- function positionFab(field) {
398
- if (!floatingBtn || !field) return;
399
- const rect = field.getBoundingClientRect();
400
- floatingBtn.style.top = `${Math.max(4, rect.top + window.scrollY + 6)}px`;
401
- floatingBtn.style.left = `${Math.max(4, rect.left + window.scrollX + 6)}px`;
402
- floatingBtn.classList.add('bayan-il-fab--visible');
403
- }
404
-
405
- function updateBadge(count) {
406
- if (!badgeCount) return;
407
- if (count === -1) {
408
- badgeCount.textContent = '…';
409
- badgeCount.className = 'bayan-il-badge bayan-il-badge--analyzing';
410
- } else if (count === 0) {
411
- badgeCount.textContent = '✓';
412
- badgeCount.className = 'bayan-il-badge bayan-il-badge--clean';
413
- } else {
414
- badgeCount.textContent = String(count);
415
- badgeCount.className = 'bayan-il-badge bayan-il-badge--errors';
416
- }
417
- }
418
-
419
- // ══════════════════════════════════════════════════════════
420
- // Utilities
421
- // ══════════════════════════════════════════════════════════
422
-
423
- function esc(text) {
424
- const m = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
425
- return String(text).replace(/[&<>"']/g, (c) => m[c]);
426
- }
427
-
428
- function errCls(type) {
429
- return { spelling: 'bayan-il-spelling', grammar: 'bayan-il-grammar', punctuation: 'bayan-il-punctuation' }[type] || 'bayan-il-spelling';
430
- }
431
-
432
- // ══════════════════════════════════════════════════════════
433
- // Field lifecycle (Task 7: no listener tracking leak)
434
- //
435
- // Per-field listeners use direct add/remove (NOT _trackedListeners).
436
- // Global listeners use addEventListener directly (cleaned on unload).
437
- // ══════════════════════════════════════════════════════════
438
-
439
- function attachField(field) {
440
- if (activeField === field) return;
441
- detachField();
442
-
443
- activeField = field;
444
- suggestions = [];
445
- if (paused) paused = false;
446
-
447
- createFloatingBtn();
448
- positionFab(field);
449
-
450
- field.addEventListener('input', onFieldInput);
451
- // FIX-20: Removed redundant 'keyup' listener (input event already fires on every change)
452
-
453
- if (BayanController.hasArabic(getFieldText(field))) {
454
- onFieldInput();
455
- }
456
- }
457
-
458
- function detachField() {
459
- if (activeField) {
460
- activeField.removeEventListener('input', onFieldInput);
461
- // FIX-20: keyup listener no longer attached
462
- activeField.removeEventListener('scroll', syncOverlay);
463
- }
464
- clearHighlights();
465
- BayanController.cancelAll();
466
- activeField = null;
467
- suggestions = [];
468
- if (floatingBtn) floatingBtn.classList.remove('bayan-il-fab--visible');
469
- }
470
-
471
- // ══════════════════════════════════════════════════════════
472
- // Global listeners
473
- // ══════════════════════════════════════════════════════════
474
-
475
- document.addEventListener('focusin', (e) => {
476
- if (isEditableField(e.target)) attachField(e.target);
477
- }, true);
478
-
479
- document.addEventListener('focusout', () => {
480
- setTimeout(() => {
481
- if (!activeField) return;
482
- if (document.activeElement === activeField) return;
483
- const a = document.activeElement;
484
- if (tooltip?.contains(a)) return;
485
- if (floatingBtn?.contains(a)) return;
486
- if (overlayContainer?.contains(a)) return;
487
- if (document.querySelector('.bayan-il-tooltip')) return;
488
- detachField();
489
- }, 300);
490
- }, true);
491
-
492
- window.addEventListener('scroll', () => {
493
- if (activeField && floatingBtn) positionFab(activeField);
494
- if (overlayContainer && activeField) {
495
- const rect = activeField.getBoundingClientRect();
496
- overlayContainer.style.top = `${rect.top + window.scrollY}px`;
497
- overlayContainer.style.left = `${rect.left + window.scrollX}px`;
498
- }
499
- }, { passive: true });
500
-
501
- window.addEventListener('resize', () => {
502
- if (activeField && floatingBtn) positionFab(activeField);
503
- }, { passive: true });
504
-
505
- // ── MutationObserver ──
506
- observer = new MutationObserver(() => {
507
- if (activeField && !document.body.contains(activeField)) detachField();
508
- });
509
- observer.observe(document.body, { childList: true, subtree: true });
510
-
511
- // ── Log ──
512
- const mode = IS_PROTECTED ? 'protected' : 'full';
513
- console.log(`[Bayan] Inline engine v7.1 (mode: ${mode}, TT: ${ttPolicy ? 'yes' : 'no'})`);
514
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 DELETED
@@ -1,55 +0,0 @@
1
- {
2
- "manifest_version": 3,
3
- "name": "__MSG_extName__",
4
- "description": "__MSG_extDescription__",
5
- "version": "2.1.0",
6
- "minimum_chrome_version": "114",
7
- "default_locale": "ar",
8
-
9
- "icons": {
10
- "16": "assets/icons/icon16.png",
11
- "48": "assets/icons/icon48.png",
12
- "128": "assets/icons/icon128.png"
13
- },
14
-
15
- "action": {
16
- "default_popup": "popup.html",
17
- "default_icon": {
18
- "16": "assets/icons/icon16.png",
19
- "48": "assets/icons/icon48.png"
20
- },
21
- "default_title": "__MSG_extName__"
22
- },
23
-
24
- "side_panel": {
25
- "default_path": "sidepanel/sidepanel.html"
26
- },
27
-
28
- "background": {
29
- "service_worker": "background.js"
30
- },
31
-
32
- "permissions": [
33
- "contextMenus",
34
- "activeTab",
35
- "storage",
36
- "sidePanel"
37
- ],
38
-
39
- "host_permissions": [
40
- "https://bayan10-bayan-api.hf.space/*"
41
- ],
42
-
43
- "content_scripts": [
44
- {
45
- "matches": ["https://*/*", "http://*/*"],
46
- "js": [
47
- "shared/constants.js",
48
- "shared/analysis-controller.js",
49
- "content-inline.js"
50
- ],
51
- "css": ["content-inline.css"],
52
- "run_at": "document_idle"
53
- }
54
- ]
55
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/popup.css DELETED
@@ -1,754 +0,0 @@
1
- /* ══════════════════════════════════════════════
2
- Bayan Chrome Extension — Popup Styles
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: #3b82f6;
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 ── */
44
- *, *::before, *::after {
45
- box-sizing: border-box;
46
- margin: 0;
47
- padding: 0;
48
- }
49
-
50
- body {
51
- width: 400px;
52
- min-height: 300px;
53
- max-height: 600px;
54
- overflow-y: auto;
55
- overflow-x: hidden;
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
- }
64
-
65
- /* Custom scrollbar */
66
- body::-webkit-scrollbar {
67
- width: 4px;
68
- }
69
- body::-webkit-scrollbar-track {
70
- background: transparent;
71
- }
72
- body::-webkit-scrollbar-thumb {
73
- background: var(--bayan-border);
74
- border-radius: 4px;
75
- }
76
-
77
- /* ══════════════════════════════════════════════
78
- Header
79
- ══════════════════════════════════════════════ */
80
- .bayan-header {
81
- display: flex;
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 {
90
- display: flex;
91
- align-items: center;
92
- gap: 8px;
93
- }
94
-
95
- .bayan-logo {
96
- filter: drop-shadow(0 0 8px var(--bayan-primary-glow));
97
- }
98
-
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;
106
- letter-spacing: -0.3px;
107
- }
108
-
109
- .bayan-header-status {
110
- display: flex;
111
- align-items: center;
112
- gap: 6px;
113
- font-size: 11px;
114
- color: var(--bayan-text-muted);
115
- }
116
-
117
- .bayan-status-dot {
118
- width: 7px;
119
- height: 7px;
120
- border-radius: 50%;
121
- background: var(--bayan-text-muted);
122
- transition: background var(--bayan-transition);
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
- /* ══════════════════════════════════════════════
134
- Tabs
135
- ══════════════════════════════════════════════ */
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: 6px;
149
- padding: 10px 16px;
150
- border: none;
151
- background: transparent;
152
- color: var(--bayan-text-muted);
153
- font-family: var(--bayan-font-arabic);
154
- font-size: 13px;
155
- font-weight: 500;
156
- cursor: pointer;
157
- transition: all var(--bayan-transition);
158
- position: relative;
159
- }
160
-
161
- .bayan-tab::after {
162
- content: '';
163
- position: absolute;
164
- bottom: 0;
165
- left: 20%;
166
- right: 20%;
167
- height: 2px;
168
- background: var(--bayan-primary);
169
- border-radius: 2px 2px 0 0;
170
- opacity: 0;
171
- transform: scaleX(0);
172
- transition: all var(--bayan-spring);
173
- }
174
-
175
- .bayan-tab:hover {
176
- color: var(--bayan-text);
177
- background: var(--bayan-surface-hover);
178
- }
179
-
180
- .bayan-tab.active {
181
- color: var(--bayan-primary-light);
182
- }
183
- .bayan-tab.active::after {
184
- opacity: 1;
185
- transform: scaleX(1);
186
- }
187
-
188
- /* ══════════════════════════════════════════════
189
- Panels
190
- ═════���════════════════════════════════════════ */
191
- .bayan-panel {
192
- display: none;
193
- padding: 16px;
194
- animation: fadeSlideIn 250ms ease-out;
195
- }
196
- .bayan-panel.active {
197
- display: block;
198
- }
199
-
200
- @keyframes fadeSlideIn {
201
- from {
202
- opacity: 0;
203
- transform: translateY(6px);
204
- }
205
- to {
206
- opacity: 1;
207
- transform: translateY(0);
208
- }
209
- }
210
-
211
- /* ══════════════════════════════════════════════
212
- Input Group
213
- ══════════════════════════════════════════════ */
214
- .bayan-input-group {
215
- margin-bottom: 12px;
216
- }
217
-
218
- .bayan-label {
219
- display: block;
220
- font-size: 12px;
221
- font-weight: 600;
222
- color: var(--bayan-text-secondary);
223
- margin-bottom: 6px;
224
- }
225
-
226
- .bayan-textarea {
227
- width: 100%;
228
- min-height: 100px;
229
- padding: 12px 14px;
230
- border: 1px solid var(--bayan-border);
231
- border-radius: var(--bayan-radius);
232
- background: var(--bayan-surface);
233
- color: var(--bayan-text);
234
- font-family: var(--bayan-font-arabic);
235
- font-size: 14px;
236
- line-height: 1.8;
237
- resize: vertical;
238
- transition: border-color var(--bayan-transition), box-shadow var(--bayan-transition);
239
- direction: rtl;
240
- }
241
-
242
- .bayan-textarea:focus {
243
- outline: none;
244
- border-color: var(--bayan-primary);
245
- box-shadow: 0 0 0 3px var(--bayan-primary-glow);
246
- }
247
-
248
- .bayan-textarea::placeholder {
249
- color: var(--bayan-text-muted);
250
- }
251
-
252
- .bayan-input-meta {
253
- display: flex;
254
- gap: 4px;
255
- margin-top: 6px;
256
- font-size: 11px;
257
- color: var(--bayan-text-muted);
258
- }
259
-
260
- .bayan-separator {
261
- margin: 0 4px;
262
- opacity: 0.4;
263
- }
264
-
265
- /* ══════════════════════════════════════════════
266
- Buttons
267
- ══════════════════════════════════════════════ */
268
- .bayan-actions {
269
- display: flex;
270
- gap: 8px;
271
- margin-bottom: 14px;
272
- }
273
-
274
- .bayan-btn {
275
- display: inline-flex;
276
- align-items: center;
277
- justify-content: center;
278
- gap: 6px;
279
- padding: 9px 18px;
280
- border: none;
281
- border-radius: var(--bayan-radius-sm);
282
- font-family: var(--bayan-font-arabic);
283
- font-size: 13px;
284
- font-weight: 600;
285
- cursor: pointer;
286
- transition: all var(--bayan-transition);
287
- }
288
-
289
- .bayan-btn-primary {
290
- flex: 1;
291
- background: linear-gradient(135deg, var(--bayan-primary), var(--bayan-primary-dark));
292
- color: white;
293
- box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
294
- }
295
- .bayan-btn-primary:hover {
296
- box-shadow: 0 4px 16px rgba(99, 102, 241, 0.45);
297
- transform: translateY(-1px);
298
- }
299
- .bayan-btn-primary:active {
300
- transform: translateY(0);
301
- box-shadow: 0 1px 4px rgba(99, 102, 241, 0.2);
302
- }
303
-
304
- .bayan-btn-ghost {
305
- background: transparent;
306
- color: var(--bayan-text-secondary);
307
- border: 1px solid var(--bayan-border);
308
- }
309
- .bayan-btn-ghost:hover {
310
- background: var(--bayan-surface-hover);
311
- color: var(--bayan-text);
312
- }
313
-
314
- .bayan-btn-sm {
315
- padding: 5px 12px;
316
- font-size: 11px;
317
- background: var(--bayan-primary);
318
- color: white;
319
- border-radius: var(--bayan-radius-sm);
320
- }
321
- .bayan-btn-sm:hover {
322
- background: var(--bayan-primary-dark);
323
- }
324
-
325
- .bayan-btn-icon {
326
- display: flex;
327
- align-items: center;
328
- justify-content: center;
329
- width: 28px;
330
- height: 28px;
331
- padding: 0;
332
- border: none;
333
- border-radius: var(--bayan-radius-sm);
334
- background: transparent;
335
- color: var(--bayan-text-muted);
336
- cursor: pointer;
337
- transition: all var(--bayan-transition);
338
- }
339
- .bayan-btn-icon:hover {
340
- background: var(--bayan-surface-hover);
341
- color: var(--bayan-text);
342
- }
343
-
344
- /* ══════════════════════════════════════════════
345
- Score Ring
346
- ══════════════════════════════════════════════ */
347
- .bayan-score-section {
348
- display: flex;
349
- align-items: center;
350
- gap: 14px;
351
- padding: 12px;
352
- margin-bottom: 14px;
353
- background: var(--bayan-surface);
354
- border: 1px solid var(--bayan-border);
355
- border-radius: var(--bayan-radius);
356
- animation: fadeSlideIn 300ms ease-out;
357
- }
358
-
359
- .bayan-score-ring {
360
- position: relative;
361
- width: 80px;
362
- height: 80px;
363
- flex-shrink: 0;
364
- }
365
-
366
- .bayan-score-ring svg {
367
- display: block;
368
- }
369
-
370
- .bayan-score-ring circle:last-child {
371
- transition: stroke-dashoffset 600ms cubic-bezier(0.34, 1.56, 0.64, 1);
372
- }
373
-
374
- .bayan-score-value {
375
- position: absolute;
376
- inset: 0;
377
- display: flex;
378
- align-items: center;
379
- justify-content: center;
380
- font-size: 20px;
381
- font-weight: 700;
382
- color: var(--bayan-primary-light);
383
- }
384
-
385
- .bayan-score-meta {
386
- flex: 1;
387
- }
388
-
389
- .bayan-score-hint {
390
- display: block;
391
- font-size: 12px;
392
- color: var(--bayan-text-secondary);
393
- margin-bottom: 8px;
394
- }
395
-
396
- .bayan-counts {
397
- display: flex;
398
- gap: 10px;
399
- flex-wrap: wrap;
400
- }
401
-
402
- .bayan-count {
403
- font-size: 11px;
404
- color: var(--bayan-text-muted);
405
- display: flex;
406
- align-items: center;
407
- gap: 3px;
408
- }
409
- .bayan-count span {
410
- font-weight: 700;
411
- }
412
- .bayan-count-spelling span { color: var(--bayan-spelling); }
413
- .bayan-count-grammar span { color: var(--bayan-grammar); }
414
- .bayan-count-punctuation span { color: var(--bayan-punctuation); }
415
-
416
- /* ══════════════════════════════════════════════
417
- Result
418
- ══════════════════════════════════════════════ */
419
- .bayan-result {
420
- margin-bottom: 14px;
421
- animation: fadeSlideIn 300ms ease-out;
422
- }
423
-
424
- .bayan-result-header {
425
- display: flex;
426
- align-items: center;
427
- justify-content: space-between;
428
- margin-bottom: 8px;
429
- }
430
-
431
- .bayan-result-title {
432
- font-size: 12px;
433
- font-weight: 600;
434
- color: var(--bayan-text-secondary);
435
- }
436
-
437
- .bayan-result-text {
438
- padding: 12px 14px;
439
- background: var(--bayan-surface);
440
- border: 1px solid var(--bayan-border);
441
- border-radius: var(--bayan-radius);
442
- font-size: 14px;
443
- line-height: 2;
444
- color: var(--bayan-text);
445
- min-height: 60px;
446
- word-wrap: break-word;
447
- }
448
-
449
- /* Error highlights in result */
450
- .bayan-spelling-error {
451
- background: rgba(239, 68, 68, 0.15);
452
- border-bottom: 2px solid var(--bayan-spelling);
453
- border-radius: 2px;
454
- padding: 0 2px;
455
- cursor: pointer;
456
- transition: background var(--bayan-transition);
457
- }
458
- .bayan-spelling-error:hover {
459
- background: rgba(239, 68, 68, 0.25);
460
- }
461
-
462
- .bayan-grammar-error {
463
- background: rgba(245, 158, 11, 0.15);
464
- border-bottom: 2px solid var(--bayan-grammar);
465
- border-radius: 2px;
466
- padding: 0 2px;
467
- cursor: pointer;
468
- transition: background var(--bayan-transition);
469
- }
470
- .bayan-grammar-error:hover {
471
- background: rgba(245, 158, 11, 0.25);
472
- }
473
-
474
- .bayan-punctuation-suggestion {
475
- background: rgba(59, 130, 246, 0.15);
476
- border-bottom: 2px solid var(--bayan-punctuation);
477
- border-radius: 2px;
478
- padding: 0 2px;
479
- cursor: pointer;
480
- transition: background var(--bayan-transition);
481
- }
482
- .bayan-punctuation-suggestion:hover {
483
- background: rgba(59, 130, 246, 0.25);
484
- }
485
-
486
- /* ══════════════════════════════════════════════
487
- Suggestions List
488
- ══════════════════════════════════════════════ */
489
- .bayan-suggestions {
490
- animation: fadeSlideIn 300ms ease-out;
491
- }
492
-
493
- .bayan-suggestions-header {
494
- display: flex;
495
- align-items: center;
496
- justify-content: space-between;
497
- margin-bottom: 10px;
498
- }
499
-
500
- .bayan-suggestions-title {
501
- font-size: 12px;
502
- font-weight: 600;
503
- color: var(--bayan-text-secondary);
504
- }
505
-
506
- .bayan-suggestions-list {
507
- display: flex;
508
- flex-direction: column;
509
- gap: 8px;
510
- }
511
-
512
- .bayan-suggestion-card {
513
- padding: 10px 12px;
514
- background: var(--bayan-surface);
515
- border: 1px solid var(--bayan-border);
516
- border-radius: var(--bayan-radius);
517
- cursor: pointer;
518
- transition: all var(--bayan-transition);
519
- }
520
- .bayan-suggestion-card:hover {
521
- border-color: var(--bayan-primary);
522
- background: var(--bayan-surface-hover);
523
- box-shadow: 0 0 0 1px var(--bayan-primary-glow);
524
- }
525
-
526
- .bayan-suggestion-badge {
527
- display: inline-block;
528
- padding: 2px 8px;
529
- border-radius: 20px;
530
- font-size: 10px;
531
- font-weight: 700;
532
- margin-bottom: 6px;
533
- }
534
-
535
- .bayan-badge-spelling {
536
- background: rgba(239, 68, 68, 0.15);
537
- color: var(--bayan-spelling);
538
- }
539
- .bayan-badge-grammar {
540
- background: rgba(245, 158, 11, 0.15);
541
- color: var(--bayan-grammar);
542
- }
543
- .bayan-badge-punctuation {
544
- background: rgba(59, 130, 246, 0.15);
545
- color: var(--bayan-punctuation);
546
- }
547
-
548
- .bayan-suggestion-change {
549
- display: flex;
550
- align-items: center;
551
- gap: 6px;
552
- margin-bottom: 8px;
553
- font-size: 13px;
554
- }
555
-
556
- .bayan-suggestion-original {
557
- text-decoration: line-through;
558
- opacity: 0.55;
559
- color: var(--bayan-error);
560
- }
561
-
562
- .bayan-suggestion-arrow {
563
- color: var(--bayan-text-muted);
564
- font-size: 12px;
565
- }
566
-
567
- .bayan-suggestion-correction {
568
- font-weight: 600;
569
- color: var(--bayan-success);
570
- }
571
-
572
- .bayan-suggestion-alts {
573
- display: flex;
574
- flex-wrap: wrap;
575
- gap: 6px;
576
- }
577
-
578
- .bayan-alt-chip {
579
- padding: 4px 10px;
580
- border: 1px solid var(--bayan-border);
581
- border-radius: 20px;
582
- background: transparent;
583
- color: var(--bayan-text-secondary);
584
- font-family: var(--bayan-font-arabic);
585
- font-size: 11px;
586
- cursor: pointer;
587
- transition: all var(--bayan-transition);
588
- }
589
- .bayan-alt-chip:hover {
590
- border-color: var(--bayan-primary);
591
- color: var(--bayan-text);
592
- background: var(--bayan-surface-active);
593
- }
594
-
595
- .bayan-alt-chip--main {
596
- background: var(--bayan-primary);
597
- border-color: var(--bayan-primary);
598
- color: white;
599
- }
600
- .bayan-alt-chip--main:hover {
601
- background: var(--bayan-primary-dark);
602
- color: white;
603
- }
604
-
605
- .bayan-alt-chip--keep {
606
- opacity: 0.65;
607
- }
608
- .bayan-alt-chip--keep:hover {
609
- opacity: 1;
610
- }
611
-
612
- /* ══��═══════════════════════════════════════════
613
- Timing
614
- ══════════════════════════════════════════════ */
615
- .bayan-timing {
616
- padding: 8px 0;
617
- margin-top: 8px;
618
- border-top: 1px solid var(--bayan-border);
619
- font-size: 10px;
620
- color: var(--bayan-text-muted);
621
- text-align: center;
622
- }
623
-
624
- /* ══════════════════════════════════════════════
625
- Summary Meta
626
- ══════════════════════════════════════════════ */
627
- .bayan-summary-meta {
628
- margin-top: 10px;
629
- padding: 8px 12px;
630
- background: var(--bayan-surface-hover);
631
- border-radius: var(--bayan-radius-sm);
632
- font-size: 11px;
633
- color: var(--bayan-text-muted);
634
- }
635
-
636
- /* ══════════════════════════════════════════════
637
- Length Selector
638
- ══════════════════════════════════════════════ */
639
- .bayan-length-selector {
640
- margin-bottom: 12px;
641
- }
642
-
643
- .bayan-radio-group {
644
- display: flex;
645
- gap: 12px;
646
- }
647
-
648
- .bayan-radio {
649
- display: flex;
650
- align-items: center;
651
- gap: 4px;
652
- font-size: 12px;
653
- color: var(--bayan-text-secondary);
654
- cursor: pointer;
655
- }
656
-
657
- .bayan-radio input[type="radio"] {
658
- accent-color: var(--bayan-primary);
659
- width: 14px;
660
- height: 14px;
661
- cursor: pointer;
662
- }
663
-
664
- /* ══════════════════════════════════════════════
665
- Loading Overlay
666
- ══════════════════════════════════════════════ */
667
- .bayan-loading {
668
- position: fixed;
669
- inset: 0;
670
- display: flex;
671
- flex-direction: column;
672
- align-items: center;
673
- justify-content: center;
674
- gap: 14px;
675
- background: rgba(15, 15, 20, 0.85);
676
- backdrop-filter: blur(4px);
677
- z-index: 100;
678
- }
679
-
680
- .bayan-spinner {
681
- width: 36px;
682
- height: 36px;
683
- border: 3px solid var(--bayan-border);
684
- border-top-color: var(--bayan-primary);
685
- border-radius: 50%;
686
- animation: spin 0.8s linear infinite;
687
- }
688
-
689
- @keyframes spin {
690
- to { transform: rotate(360deg); }
691
- }
692
-
693
- .bayan-loading-text {
694
- font-size: 13px;
695
- color: var(--bayan-text-secondary);
696
- }
697
-
698
- /* ══════════════════════════════════════════════
699
- Toast
700
- ══════════════════════════════════════════════ */
701
- .bayan-toast {
702
- position: fixed;
703
- bottom: -50px;
704
- left: 50%;
705
- transform: translateX(-50%);
706
- padding: 8px 18px;
707
- background: var(--bayan-surface);
708
- border: 1px solid var(--bayan-border);
709
- border-radius: 20px;
710
- color: var(--bayan-text);
711
- font-size: 12px;
712
- font-family: var(--bayan-font-arabic);
713
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
714
- transition: bottom var(--bayan-spring);
715
- z-index: 200;
716
- white-space: nowrap;
717
- }
718
- .bayan-toast.is-visible {
719
- bottom: 16px;
720
- }
721
-
722
- /* ══════════════════════════════════════════════
723
- MED-2: Stale analysis indicator
724
- ══════════════════════════════════════════════ */
725
- .bayan-stale {
726
- opacity: 0.45;
727
- pointer-events: none;
728
- position: relative;
729
- filter: grayscale(30%);
730
- transition: opacity var(--bayan-transition), filter var(--bayan-transition);
731
- }
732
- .bayan-stale::after {
733
- content: '⚠ أعد التحليل';
734
- position: absolute;
735
- top: 8px;
736
- left: 50%;
737
- transform: translateX(-50%);
738
- padding: 4px 12px;
739
- background: var(--bayan-warning);
740
- color: #000;
741
- font-size: 10px;
742
- font-weight: 700;
743
- border-radius: 12px;
744
- z-index: 10;
745
- pointer-events: none;
746
- white-space: nowrap;
747
- }
748
-
749
- /* ══════════════════════════════════════════════
750
- Utilities
751
- ══════════════════════════════════════════════ */
752
- .is-hidden {
753
- display: none !important;
754
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/popup.html DELETED
@@ -1,199 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="ar" dir="rtl">
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>
10
- <!-- ══════════════════════════════════════════════ -->
11
- <!-- Header -->
12
- <!-- ══════════════════════════════════════════════ -->
13
- <header class="bayan-header">
14
- <div class="bayan-header-brand">
15
- <svg class="bayan-logo" width="28" height="28" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
16
- <circle cx="50" cy="50" r="46" fill="url(#logoGrad)" />
17
- <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)" />
18
- <circle cx="50" cy="42" r="4" fill="url(#logoGrad)" />
19
- <defs>
20
- <linearGradient id="logoGrad" x1="0" y1="0" x2="100" y2="100">
21
- <stop offset="0%" stop-color="#6366f1"/>
22
- <stop offset="100%" stop-color="#8b5cf6"/>
23
- </linearGradient>
24
- </defs>
25
- </svg>
26
- <span class="bayan-header-title">بيان</span>
27
- </div>
28
- <div class="bayan-header-status" id="status-indicator">
29
- <span class="bayan-status-dot"></span>
30
- <span class="bayan-status-text" id="status-text">جاهز</span>
31
- </div>
32
- </header>
33
-
34
- <!-- ══════════════════════════════════════════════ -->
35
- <!-- Tab Navigation -->
36
- <!-- ══════════════════════════════════════════════ -->
37
- <nav class="bayan-tabs" role="tablist">
38
- <button class="bayan-tab active" role="tab" aria-selected="true" data-tab="correct" id="tab-correct">
39
- <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-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
40
- تصحيح
41
- </button>
42
- <button class="bayan-tab" role="tab" aria-selected="false" data-tab="summarize" id="tab-summarize">
43
- <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 6h16M4 12h10M4 18h14"/></svg>
44
- تلخيص
45
- </button>
46
- </nav>
47
-
48
- <!-- ══════════════════════════════════════════════ -->
49
- <!-- Correct Panel -->
50
- <!-- ══════════════════════════════════════════════ -->
51
- <section class="bayan-panel active" id="panel-correct" role="tabpanel">
52
- <!-- Input area -->
53
- <div class="bayan-input-group">
54
- <label class="bayan-label" for="input-text">أدخل النص العربي</label>
55
- <textarea
56
- id="input-text"
57
- class="bayan-textarea"
58
- dir="rtl"
59
- placeholder="الصق أو اكتب النص هنا..."
60
- rows="5"
61
- maxlength="5000"
62
- ></textarea>
63
- <div class="bayan-input-meta">
64
- <span id="char-count">٠</span> حرف
65
- <span class="bayan-separator">|</span>
66
- <span id="word-count">٠</span> كلمة
67
- </div>
68
- </div>
69
-
70
- <!-- Action buttons -->
71
- <div class="bayan-actions">
72
- <button class="bayan-btn bayan-btn-primary" id="btn-correct" type="button">
73
- <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>
74
- تحليل وتصحيح
75
- </button>
76
- <button class="bayan-btn bayan-btn-ghost" id="btn-clear" type="button">مسح</button>
77
- </div>
78
-
79
- <!-- Score ring -->
80
- <div class="bayan-score-section is-hidden" id="score-section">
81
- <div class="bayan-score-ring">
82
- <svg viewBox="0 0 160 160" width="80" height="80">
83
- <circle cx="80" cy="80" r="70" fill="none" stroke="var(--bayan-border)" stroke-width="8"/>
84
- <circle cx="80" cy="80" r="70" fill="none" stroke="var(--bayan-primary)" stroke-width="8"
85
- stroke-dasharray="440" stroke-dashoffset="440" stroke-linecap="round"
86
- transform="rotate(-90 80 80)" id="score-circle"/>
87
- </svg>
88
- <span class="bayan-score-value" id="score-value">--</span>
89
- </div>
90
- <div class="bayan-score-meta">
91
- <span class="bayan-score-hint" id="score-hint">ابدأ الكتابة لرؤية تقييمك</span>
92
- <div class="bayan-counts" id="error-counts">
93
- <span class="bayan-count bayan-count-spelling"><span id="count-spelling">٠</span> إملائي</span>
94
- <span class="bayan-count bayan-count-grammar"><span id="count-grammar">٠</span> نحوي</span>
95
- <span class="bayan-count bayan-count-punctuation"><span id="count-punctuation">٠</span> ترقيم</span>
96
- </div>
97
- </div>
98
- </div>
99
-
100
- <!-- Corrected text result -->
101
- <div class="bayan-result is-hidden" id="result-section">
102
- <div class="bayan-result-header">
103
- <span class="bayan-result-title">النص المصحّح</span>
104
- <button class="bayan-btn-icon" id="btn-copy-result" type="button" title="نسخ">
105
- <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>
106
- </button>
107
- </div>
108
- <div class="bayan-result-text" id="result-text" dir="rtl"></div>
109
- </div>
110
-
111
- <!-- Suggestions list -->
112
- <div class="bayan-suggestions is-hidden" id="suggestions-section">
113
- <div class="bayan-suggestions-header">
114
- <span class="bayan-suggestions-title">الاقتراحات</span>
115
- <button class="bayan-btn bayan-btn-sm" id="btn-apply-all" type="button">تطبيق الكل</button>
116
- </div>
117
- <div class="bayan-suggestions-list" id="suggestions-list" role="list"></div>
118
- </div>
119
-
120
- <!-- Timing info -->
121
- <div class="bayan-timing is-hidden" id="timing-section">
122
- <span id="timing-text"></span>
123
- </div>
124
- </section>
125
-
126
- <!-- ══════════════════════════════════════════════ -->
127
- <!-- Summarize Panel -->
128
- <!-- ══════════════════════════════════════════════ -->
129
- <section class="bayan-panel" id="panel-summarize" role="tabpanel">
130
- <!-- Input area -->
131
- <div class="bayan-input-group">
132
- <label class="bayan-label" for="summary-input-text">أدخل النص للتلخيص</label>
133
- <textarea
134
- id="summary-input-text"
135
- class="bayan-textarea"
136
- dir="rtl"
137
- placeholder="الصق النص المراد تلخيصه..."
138
- rows="5"
139
- maxlength="5000"
140
- ></textarea>
141
- <div class="bayan-input-meta">
142
- <span id="summary-char-count">٠</span> حرف
143
- </div>
144
- </div>
145
-
146
- <!-- Length selector -->
147
- <div class="bayan-length-selector">
148
- <label class="bayan-label">طول الملخص</label>
149
- <div class="bayan-radio-group">
150
- <label class="bayan-radio"><input type="radio" name="summary-length" value="1"> قصير</label>
151
- <label class="bayan-radio"><input type="radio" name="summary-length" value="2" checked> متوسط</label>
152
- <label class="bayan-radio"><input type="radio" name="summary-length" value="3"> طويل</label>
153
- </div>
154
- </div>
155
-
156
- <!-- Action -->
157
- <div class="bayan-actions">
158
- <button class="bayan-btn bayan-btn-primary" id="btn-summarize" type="button">
159
- <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 6h16M4 12h10M4 18h14"/></svg>
160
- تلخيص
161
- </button>
162
- </div>
163
-
164
- <!-- Summary result -->
165
- <div class="bayan-result is-hidden" id="summary-result-section">
166
- <div class="bayan-result-header">
167
- <span class="bayan-result-title">الملخص</span>
168
- <button class="bayan-btn-icon" id="btn-copy-summary" type="button" title="نسخ">
169
- <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>
170
- </button>
171
- </div>
172
- <div class="bayan-result-text" id="summary-text" dir="rtl"></div>
173
- <div class="bayan-summary-meta" id="summary-meta"></div>
174
- </div>
175
- </section>
176
-
177
- <!-- ══════════════════════════════════════════════ -->
178
- <!-- Loading overlay -->
179
- <!-- ══════════════════════════════════════════════ -->
180
- <div class="bayan-loading is-hidden" id="loading-overlay">
181
- <div class="bayan-spinner"></div>
182
- <span class="bayan-loading-text" id="loading-text">جارٍ التحليل...</span>
183
- </div>
184
-
185
- <!-- ══════════════════════════════════════════════ -->
186
- <!-- Toast -->
187
- <!-- ══════════════════════════════════════════════ -->
188
- <div class="bayan-toast" id="toast" role="status" aria-live="polite"></div>
189
-
190
- <!-- Scripts: constants first, then config, then shared modules, then popup logic -->
191
- <script src="shared/constants.js"></script>
192
- <script src="shared/config.js"></script>
193
- <script src="shared/bayan-renderer.js"></script>
194
- <script src="shared/bayan-ui.js"></script>
195
- <script src="shared/bayan-patches.js"></script>
196
- <script src="shared/bayan-api.js"></script>
197
- <script src="popup.js"></script>
198
- </body>
199
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/popup.js DELETED
@@ -1,497 +0,0 @@
1
- /**
2
- * Bayan Chrome Extension — Popup Logic
3
- *
4
- * Fixes applied:
5
- * HIGH-1: Offset rebasing via applyAndRebase() after each individual apply
6
- * MED-1: Clipboard .catch() on all writeText() calls
7
- * MED-2: Staleness detection — locks corrections when user edits after analysis
8
- */
9
-
10
- document.addEventListener('DOMContentLoaded', () => {
11
- // ── Element references ──
12
- const inputText = document.getElementById('input-text');
13
- const charCount = document.getElementById('char-count');
14
- const wordCount = document.getElementById('word-count');
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');
25
- const timingText = document.getElementById('timing-text');
26
- const loadingOverlay = document.getElementById('loading-overlay');
27
- const loadingTextEl = document.getElementById('loading-text');
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
39
- const scoreValue = document.getElementById('score-value');
40
- const scoreCircle = document.getElementById('score-circle');
41
- const scoreHint = document.getElementById('score-hint');
42
- const countSpelling = document.getElementById('count-spelling');
43
- const countGrammar = document.getElementById('count-grammar');
44
- const countPunctuation = document.getElementById('count-punctuation');
45
-
46
- // ══════════════════════════════════════════════════════════
47
- // State
48
- // ══════════════════════════════════════════════════════════
49
- let currentSuggestions = [];
50
-
51
- /**
52
- * MED-2: Snapshot of the text that was last analyzed.
53
- * All suggestion offsets are relative to THIS text.
54
- * applyAndRebase() mutates this alongside suggestions.
55
- */
56
- let analyzedText = '';
57
-
58
- /**
59
- * MED-2: Whether the analysis results are stale.
60
- * Set to true when the user edits the textarea after analysis.
61
- * When stale, suggestion actions are blocked.
62
- */
63
- let isStale = false;
64
-
65
- const SCORE_CIRCUMFERENCE = 440;
66
-
67
- // ══════════════════════════════════════════════════════════
68
- // Tab switching
69
- // ══════════════════════════════════════════════════════════
70
- document.querySelectorAll('.bayan-tab').forEach((tab) => {
71
- tab.addEventListener('click', () => {
72
- const targetTab = tab.dataset.tab;
73
- document.querySelectorAll('.bayan-tab').forEach((t) => {
74
- t.classList.toggle('active', t.dataset.tab === targetTab);
75
- t.setAttribute('aria-selected', t.dataset.tab === targetTab ? 'true' : 'false');
76
- });
77
- document.querySelectorAll('.bayan-panel').forEach((p) => {
78
- p.classList.toggle('active', p.id === `panel-${targetTab}`);
79
- });
80
- });
81
- });
82
-
83
- // ══════════════════════════════════════════════════════════
84
- // Character & word counter
85
- // ══════════════════════════════════════════════════════════
86
- function updateCounts(textarea, charEl, wordEl) {
87
- const text = textarea.value;
88
- const chars = text.length;
89
- const words = text.trim() ? text.trim().split(/\s+/).length : 0;
90
- if (charEl) charEl.textContent = chars.toLocaleString('ar-EG');
91
- if (wordEl) wordEl.textContent = words.toLocaleString('ar-EG');
92
- }
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) {
99
- markStale();
100
- }
101
- });
102
-
103
- summaryInputText.addEventListener('input', () => updateCounts(summaryInputText, summaryCharCount, null));
104
-
105
- // ══════════════════════════════════════════════════════════
106
- // MED-2: Staleness management
107
- // ══════════════════════════════════════════════════════════
108
-
109
- /**
110
- * Mark analysis results as stale (user edited textarea).
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
- إعادة التحليل`;
128
- }
129
-
130
- /**
131
- * Clear staleness (after re-analysis or clear).
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
- تحليل وتصحيح`;
142
- }
143
-
144
- // ══════════════════════════════════════════════════════════
145
- // Loading state
146
- // ══════════════════════════════════════════════════════════
147
- function setLoading(show, text = 'جارٍ التحليل...') {
148
- loadingOverlay.classList.toggle('is-hidden', !show);
149
- loadingTextEl.textContent = text;
150
- }
151
-
152
- // ══════════════════════════════════════════════════════════
153
- // Toast
154
- // ══════════════════════════════════════════════════════════
155
- function showToast(message, duration = 2500) {
156
- const toast = document.getElementById('toast');
157
- toast.textContent = message;
158
- toast.classList.add('is-visible');
159
- clearTimeout(toast._timer);
160
- toast._timer = setTimeout(() => toast.classList.remove('is-visible'), duration);
161
- }
162
-
163
- // ══════════════════════════════════════════════════════════
164
- // Score ring
165
- // ══════════════════════════════════════════════════════════
166
- function updateScore(spelling, grammar, punctuation) {
167
- const score = calculateWritingScore(spelling, grammar, punctuation);
168
- const total = spelling + grammar + punctuation;
169
-
170
- scoreSection.classList.remove('is-hidden');
171
-
172
- if (scoreValue) {
173
- scoreValue.textContent = score > 0 || total > 0 ? score.toLocaleString('ar-EG') : '--';
174
- }
175
- if (scoreCircle) {
176
- const offset = SCORE_CIRCUMFERENCE - (score / 100) * SCORE_CIRCUMFERENCE;
177
- scoreCircle.style.strokeDashoffset = String(offset);
178
- }
179
- if (scoreHint) {
180
- scoreHint.textContent = getScoreHint(score, total);
181
- }
182
- if (countSpelling) countSpelling.textContent = spelling.toLocaleString('ar-EG');
183
- if (countGrammar) countGrammar.textContent = grammar.toLocaleString('ar-EG');
184
- if (countPunctuation) countPunctuation.textContent = punctuation.toLocaleString('ar-EG');
185
- }
186
-
187
- // ══════════════════════════════════════════════════════════
188
- // Render suggestions list
189
- // ══════════════════════════════════════════════════════════
190
- function renderSuggestions(suggestions) {
191
- currentSuggestions = suggestions;
192
-
193
- if (!suggestions || suggestions.length === 0) {
194
- suggestionsSection.classList.add('is-hidden');
195
- return;
196
- }
197
-
198
- suggestionsSection.classList.remove('is-hidden');
199
- const html = suggestions.map((s, i) => buildSuggestionCardHTML(s, i)).join('');
200
- suggestionsList.innerHTML = html;
201
-
202
- // Bind suggestion card events
203
- suggestionsList.querySelectorAll('.bayan-alt-chip').forEach((chip) => {
204
- chip.addEventListener('click', (e) => {
205
- e.stopPropagation();
206
-
207
- // MED-2: Block if stale
208
- if (isStale) {
209
- showToast('⚠ أعد التحليل أولاً — النص تغيّر');
210
- return;
211
- }
212
-
213
- const suggestionId = chip.dataset.cardId;
214
- const altText = chip.dataset.cardAlt;
215
- const suggestion = currentSuggestions.find((s) => String(s.id || '') === String(suggestionId));
216
- if (!suggestion) return;
217
-
218
- if (altText === suggestion.original) {
219
- // Dismiss — remove from list, no text change, no rebase needed
220
- currentSuggestions = removeSuggestion(currentSuggestions, suggestion.id);
221
- } else {
222
- // ═══════════════════════════════════════════════════
223
- // HIGH-1 FIX: Apply + Rebase via atomic function
224
- // ═══════════════════════════════════════════════════
225
- const result = applyAndRebase(analyzedText, suggestion, altText, currentSuggestions);
226
- analyzedText = result.text;
227
- currentSuggestions = result.suggestions;
228
-
229
- // Sync textarea with the rebased text
230
- inputText.value = analyzedText;
231
- updateCounts(inputText, charCount, wordCount);
232
- }
233
-
234
- // Re-render score and suggestions
235
- const counts = countByType(currentSuggestions);
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
-
246
- // Show apply-all only when >= 2
247
- btnApplyAll.classList.toggle('is-hidden', suggestions.length < 2);
248
- }
249
-
250
- // ══════════════════════════════════════════════════════════
251
- // Clear button
252
- // ══════════════════════════════════════════════════════════
253
- btnClear.addEventListener('click', () => {
254
- inputText.value = '';
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
- // ══════════════════════════════════════════════════════════
266
- // Correct button
267
- // ══════════════════════════════════════════════════════════
268
- btnCorrect.addEventListener('click', async () => {
269
- const text = inputText.value.trim();
270
- if (!text) {
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) {
279
- showToast('النص طويل جداً (الحد الأقصى ٥٠٠٠ حرف)');
280
- return;
281
- }
282
-
283
- setLoading(true, 'جارٍ التحليل...');
284
- clearStale(); // MED-2: Clear stale on re-analysis
285
-
286
- try {
287
- const data = await bayanAnalyze(text);
288
-
289
- if (data.status === 'success' || data.status === 'partial') {
290
- const suggestions = sortSuggestions(data.suggestions || []);
291
- currentSuggestions = suggestions;
292
-
293
- // MED-2: Snapshot the analyzed text — all offsets reference THIS string
294
- analyzedText = data.original;
295
- // Sync textarea to the exact text the backend analyzed
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);
305
- updateScore(counts.spelling, counts.grammar, counts.punctuation);
306
-
307
- // Render suggestion cards
308
- renderSuggestions(suggestions);
309
-
310
- // Show timing
311
- if (data.timing_ms) {
312
- timingSection.classList.remove('is-hidden');
313
- timingText.textContent = `التحليل: ${data.timing_ms.total_ms || 0}ms (إملائي: ${data.timing_ms.spelling_ms || 0}ms، نحوي: ${data.timing_ms.grammar_ms || 0}ms، ترقيم: ${data.timing_ms.punctuation_ms || 0}ms)`;
314
- }
315
-
316
- if (suggestions.length === 0) {
317
- showToast('نصك ممتاز! لم نجد أي أخطاء ✨');
318
- }
319
- } else {
320
- showToast('تعذّر التحليل — حاول مرة أخرى');
321
- }
322
- } catch (error) {
323
- console.error('[Bayan] Analysis error:', error);
324
- showToast('خطأ في الاتصال — تحقق من الإنترنت');
325
- } finally {
326
- setLoading(false);
327
- }
328
- });
329
-
330
- // ══════════════════════════════════════════════════════════
331
- // Apply all button
332
- // ══════════════════════════════════════════════════════════
333
- btnApplyAll.addEventListener('click', () => {
334
- if (currentSuggestions.length === 0) return;
335
-
336
- // MED-2: Block if stale
337
- if (isStale) {
338
- showToast('⚠ أعد التحليل أولاً — النص تغيّر');
339
- return;
340
- }
341
-
342
- // Apply all patches using reverse-order (no rebase needed — all applied at once)
343
- analyzedText = applyAllPatches(analyzedText, currentSuggestions);
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
- // ══════════════════════════════════════════════════════════
366
- btnSummarize.addEventListener('click', async () => {
367
- const text = summaryInputText.value.trim();
368
- if (!text) {
369
- showToast('أدخل نصاً للتلخيص');
370
- return;
371
- }
372
- if (text.length < CONFIG.MIN_SUMMARIZE_LENGTH) {
373
- showToast('النص قصير جداً للتلخيص');
374
- return;
375
- }
376
-
377
- const lengthValue = parseInt(document.querySelector('input[name="summary-length"]:checked')?.value || '2', 10);
378
-
379
- setLoading(true, 'جارٍ التلخيص...');
380
-
381
- try {
382
- const data = await bayanSummarize(text, lengthValue);
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('تعذّر التلخيص — حاول مرة أخرى');
391
- }
392
- } catch (error) {
393
- console.error('[Bayan] Summarization error:', error);
394
- showToast('خطأ في الاتصال — تحقق من الإنترنت');
395
- } finally {
396
- setLoading(false);
397
- }
398
- });
399
-
400
- // ══════════════════════════════════════════════════════════
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('تعذّر النسخ'));
408
- });
409
-
410
- // ══════════════════════════��═══════════════════════════════
411
- // Status check on load
412
- // ══════════════════════════════════════════════════════════
413
- (async function checkStatus() {
414
- const statusDot = document.querySelector('.bayan-status-dot');
415
- const statusText = document.getElementById('status-text');
416
- try {
417
- await bayanHealthCheck();
418
- if (statusDot) statusDot.classList.add('online');
419
- if (statusText) statusText.textContent = 'متصل';
420
- } catch {
421
- if (statusDot) statusDot.classList.add('offline');
422
- if (statusText) statusText.textContent = 'غير متصل';
423
- }
424
- })();
425
-
426
- // ══════════════════════════════════════════════════════════
427
- // Phase 4: Context Menu Pickup
428
- // ══════════════════════════════════════════════════════════
429
- // Fix #2: Guard against double execution
430
- // Fix #4: Tab name constants
431
- // Fix #5: Storage fallback (session → local)
432
- // ══════════════════════════════════════════════════════════
433
-
434
- const TAB_ACTIONS = { correct: 'correct', summarize: 'summarize' };
435
- let contextConsumed = false; // Fix #2
436
-
437
- (async function checkContextAction() {
438
- // Only available in extension context (not in test pages)
439
- if (typeof chrome === 'undefined' || !chrome.storage) return;
440
-
441
- // Fix #2: Prevent double-trigger
442
- if (contextConsumed) return;
443
-
444
- // Fix #5: Storage fallback
445
- const storage = chrome.storage?.session || chrome.storage?.local;
446
- if (!storage) return;
447
-
448
- try {
449
- const data = await storage.get(['contextAction', 'contextText', 'contextTimestamp']);
450
- if (!data.contextAction || !data.contextText) return;
451
-
452
- // Ignore stale actions (older than 15 seconds — matches background cleanup)
453
- const age = Date.now() - (data.contextTimestamp || 0);
454
- if (age > 15000) {
455
- chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
456
- return;
457
- }
458
-
459
- // Fix #2: Mark as consumed BEFORE processing
460
- contextConsumed = true;
461
-
462
- console.log(`[Bayan] Context action: ${data.contextAction}, text length: ${data.contextText.length}`);
463
-
464
- if (data.contextAction === TAB_ACTIONS.correct) {
465
- // Fill the correction tab and auto-trigger analysis
466
- inputText.value = data.contextText;
467
- updateCounts(inputText, charCount, wordCount);
468
-
469
- // Switch to correction tab
470
- const correctTab = document.querySelector(`[data-tab="${TAB_ACTIONS.correct}"]`);
471
- if (correctTab) correctTab.click();
472
-
473
- // Auto-click the correct button after a brief delay for UI paint
474
- setTimeout(() => btnCorrect.click(), 150);
475
-
476
- } else if (data.contextAction === TAB_ACTIONS.summarize) {
477
- // Fill the summary tab and auto-trigger summarization
478
- summaryInputText.value = data.contextText;
479
- updateCounts(summaryInputText, summaryCharCount, null);
480
-
481
- // Switch to summarize tab
482
- const summarizeTab = document.querySelector(`[data-tab="${TAB_ACTIONS.summarize}"]`);
483
- if (summarizeTab) summarizeTab.click();
484
-
485
- // Auto-click the summarize button
486
- setTimeout(() => btnSummarize.click(), 150);
487
- }
488
-
489
- // Clear the context action so it doesn't re-trigger on next popup open
490
- chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
491
-
492
- } catch (err) {
493
- console.warn('[Bayan] Context action check failed:', err);
494
- }
495
- })();
496
- });
497
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/shared/analysis-controller.js DELETED
@@ -1,146 +0,0 @@
1
- /**
2
- * Bayan — Analysis Controller (Phase 7.1 Simplified)
3
- *
4
- * Responsibilities (ONLY):
5
- * - Adaptive debounce (2-tier: normal / large text)
6
- * - Stale request cancellation
7
- * - Text validation
8
- * - Message passing to background.js
9
- *
10
- * Does NOT own:
11
- * - Caching (background.js)
12
- * - Retry logic (background.js)
13
- * - Timeout (background.js)
14
- * - Network (background.js)
15
- *
16
- * Requires: BAYAN (shared/constants.js)
17
- */
18
-
19
- // eslint-disable-next-line no-unused-vars
20
- const BayanController = (() => {
21
- 'use strict';
22
-
23
- // ── Debounce config ──
24
- const DEBOUNCE_NORMAL = 500;
25
- const DEBOUNCE_LARGE = 1000;
26
- const LARGE_TEXT_THRESHOLD = 2000;
27
-
28
- // ── State ──
29
- let _debounceTimer = null;
30
- let _inflight = false;
31
-
32
- // ── Protected site check ──
33
- const _isProtected = BAYAN.PROTECTED_HOSTS.includes(window.location.hostname);
34
-
35
- // ══════════════════════════════════════════════════════════
36
- // Text validation
37
- // ══════════════════════════════════════════════════════════
38
-
39
- function hasArabic(text) {
40
- if (!text || text.length < BAYAN.MIN_TEXT_LENGTH) return false;
41
- const arabicChars = (text.match(/[\u0600-\u06FF]/g) || []).length;
42
- return arabicChars >= BAYAN.MIN_ARABIC_CHARS;
43
- }
44
-
45
- function validateText(text) {
46
- if (!text) return { valid: false, reason: 'empty' };
47
- if (text.length < BAYAN.MIN_TEXT_LENGTH) return { valid: false, reason: 'too_short' };
48
- if (text.length > BAYAN.MAX_TEXT_LENGTH) return { valid: false, reason: 'too_long' };
49
- if (!hasArabic(text)) return { valid: false, reason: 'no_arabic' };
50
- return { valid: true };
51
- }
52
-
53
- // ══════════════════════════════════════════════════════════
54
- // Core: debounced analysis via background bridge
55
- // ══════════════════════════════════════════════════════════
56
-
57
- /**
58
- * Schedule a debounced analysis.
59
- * Background.js owns caching, retry, and timeout.
60
- *
61
- * @param {string} text
62
- * @param {function(Object|null): void} onResult
63
- * @param {function(): string} getCurrentText — staleness checker
64
- */
65
- function scheduleAnalysis(text, onResult, getCurrentText) {
66
- if (_debounceTimer) {
67
- clearTimeout(_debounceTimer);
68
- _debounceTimer = null;
69
- }
70
-
71
- if (!validateText(text).valid) {
72
- onResult(null);
73
- return;
74
- }
75
-
76
- const delay = text.length > LARGE_TEXT_THRESHOLD ? DEBOUNCE_LARGE : DEBOUNCE_NORMAL;
77
-
78
- _debounceTimer = setTimeout(() => {
79
- _debounceTimer = null;
80
-
81
- // Staleness check: text changed during debounce
82
- if (getCurrentText() !== text) return;
83
-
84
- executeAnalysis(text, onResult, getCurrentText);
85
- }, delay);
86
- }
87
-
88
- /**
89
- * Send analysis request to background.js.
90
- * No timeout, no retry, no cache — background handles all of that.
91
- */
92
- function executeAnalysis(text, onResult, getCurrentText) {
93
- _inflight = true;
94
-
95
- chrome.runtime.sendMessage({ type: 'INLINE_ANALYZE', text }, (response) => {
96
- _inflight = false;
97
-
98
- // Extension context invalidated (e.g., extension reloaded)
99
- if (chrome.runtime.lastError) {
100
- console.warn('[Bayan Controller]', chrome.runtime.lastError.message);
101
- onResult(null);
102
- return;
103
- }
104
-
105
- // Staleness check: text changed during fetch
106
- if (getCurrentText() !== text) return;
107
-
108
- if (!response || response.error) {
109
- onResult(null);
110
- return;
111
- }
112
-
113
- onResult(response.data);
114
- });
115
- }
116
-
117
- // ══════════════════════════════════════════════════════════
118
- // Cancellation
119
- // ══════════════════════════════════════════════════════════
120
-
121
- function cancelAll() {
122
- if (_debounceTimer) {
123
- clearTimeout(_debounceTimer);
124
- _debounceTimer = null;
125
- }
126
- _inflight = false;
127
- }
128
-
129
- function destroy() {
130
- cancelAll();
131
- }
132
-
133
- // ══════════════════════════════════════════════════════════
134
- // Public API
135
- // ══════════════════════════════════════════════════════════
136
-
137
- return {
138
- scheduleAnalysis,
139
- cancelAll,
140
- destroy,
141
- validateText,
142
- hasArabic,
143
- isProtectedSite() { return _isProtected; },
144
- isInFlight() { return _inflight; },
145
- };
146
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/shared/bayan-api.js DELETED
@@ -1,65 +0,0 @@
1
- /**
2
- * Bayan Chrome Extension — API Client
3
- *
4
- * Reused from: src/js/api.js (L2-20)
5
- * Adaptation: Relative paths replaced with CONFIG.API_BASE.
6
- * Only analyzeText and summarizeText are needed (Phase 0 audit).
7
- */
8
-
9
- /**
10
- * Send text to the unified analysis pipeline.
11
- * Backend: /api/analyze (Spelling → Grammar → Punctuation)
12
- *
13
- * @param {string} text - Arabic text to analyze
14
- * @param {AbortSignal} [signal] - Optional abort signal
15
- * @returns {Promise<Object>} { original, corrected, suggestions[], timing_ms, status }
16
- */
17
- async function bayanAnalyze(text, signal) {
18
- const response = await fetch(`${CONFIG.API_BASE}/api/analyze`, {
19
- method: 'POST',
20
- headers: { 'Content-Type': 'application/json' },
21
- body: JSON.stringify({ text }),
22
- signal,
23
- });
24
- if (!response.ok) {
25
- throw new Error(`Analyze API error: ${response.status}`);
26
- }
27
- return await response.json();
28
- }
29
-
30
- /**
31
- * Summarize Arabic text.
32
- * Backend: /api/summarize
33
- *
34
- * @param {string} text - Arabic text to summarize
35
- * @param {number} [length=2] - Summary length (1=short, 2=medium, 3=long)
36
- * @param {boolean} [fullText=true] - Summarize full text or first paragraph
37
- * @returns {Promise<Object>} { summary, status, original_length, summary_length }
38
- */
39
- async function bayanSummarize(text, length = 2, fullText = true) {
40
- const response = await fetch(`${CONFIG.API_BASE}/api/summarize`, {
41
- method: 'POST',
42
- headers: { 'Content-Type': 'application/json' },
43
- body: JSON.stringify({ text, length, full_text: fullText }),
44
- });
45
- if (!response.ok) {
46
- throw new Error(`Summarize API error: ${response.status}`);
47
- }
48
- return await response.json();
49
- }
50
-
51
- /**
52
- * Check backend health.
53
- * Backend: /api/health
54
- *
55
- * @returns {Promise<Object>} { status, mode, models }
56
- */
57
- async function bayanHealthCheck() {
58
- const response = await fetch(`${CONFIG.API_BASE}/api/health`, {
59
- method: 'GET',
60
- });
61
- if (!response.ok) {
62
- throw new Error(`Health check error: ${response.status}`);
63
- }
64
- return await response.json();
65
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/shared/bayan-patches.js DELETED
@@ -1,152 +0,0 @@
1
- /**
2
- * Bayan Chrome Extension — Patch Application Engine
3
- *
4
- * Algorithms reused from: src/js/editor.js
5
- * Core logic: reverse-order offset replacement (editor.js L674-710)
6
- *
7
- * This module applies suggestion corrections to plain text
8
- * without requiring DOM manipulation.
9
- *
10
- * ═══════════════════════════════════════════════════════════
11
- * HIGH-1 FIX — Offset Rebasing Strategy
12
- * ═══════════════════════════════════════════════════════════
13
- *
14
- * When applying a single correction, the text length may change.
15
- * All remaining suggestions whose offsets come AFTER the applied
16
- * patch must be shifted by the length difference (delta).
17
- *
18
- * Strategy: LINEAR SHIFT
19
- * delta = replacementText.length - (suggestion.end - suggestion.start)
20
- * For every remaining suggestion S where S.start >= applied.end:
21
- * S.start += delta
22
- * S.end += delta
23
- *
24
- * Edge cases:
25
- * - Shorter replacement (delta < 0): offsets shift left
26
- * - Longer replacement (delta > 0): offsets shift right
27
- * - Same-length (delta = 0): offsets unchanged
28
- * - Overlapping suggestions: handled by PatchSet.resolve_overlaps()
29
- * on the backend — we never receive overlapping suggestions
30
- *
31
- * This is O(n) per apply, which is fine for ≤50 suggestions.
32
- * ═══════════════════════════════════════════════════════════
33
- */
34
-
35
- /**
36
- * Apply a single suggestion patch to text.
37
- * Source: src/js/editor.js L506-510 (offset-based replacement)
38
- *
39
- * @param {string} text - Original text
40
- * @param {Object} suggestion - { start, end, correction }
41
- * @returns {string} Text with correction applied
42
- */
43
- function applyPatch(text, suggestion) {
44
- const before = text.substring(0, suggestion.start);
45
- const after = text.substring(suggestion.end);
46
- return before + suggestion.correction + after;
47
- }
48
-
49
- /**
50
- * Apply all suggestion patches to text.
51
- * CRITICAL: Sort in REVERSE order (highest start offset first)
52
- * to prevent offset shifts from invalidating subsequent patches.
53
- * Source: src/js/editor.js L674-710 (reverse-order algorithm)
54
- *
55
- * @param {string} text - Original text
56
- * @param {Array} suggestions - Array of { start, end, correction }
57
- * @returns {string} Fully corrected text
58
- */
59
- function applyAllPatches(text, suggestions) {
60
- if (!suggestions || suggestions.length === 0) return text;
61
-
62
- // CRITICAL: Sort in REVERSE order (highest start offset first).
63
- // This is the exact same algorithm from editor.js L676.
64
- const sorted = [...suggestions].sort((a, b) => b.start - a.start);
65
-
66
- let result = text;
67
- sorted.forEach((s) => {
68
- result = result.substring(0, s.start) + s.correction + result.substring(s.end);
69
- });
70
-
71
- return result;
72
- }
73
-
74
- /**
75
- * Apply a specific alternative correction to text.
76
- * Source: src/js/editor.js L547-600 (alternative correction pattern)
77
- *
78
- * @param {string} text - Original text
79
- * @param {Object} suggestion - The suggestion being corrected
80
- * @param {string} alternativeText - The alternative correction to apply
81
- * @returns {string} Text with alternative correction applied
82
- */
83
- function applyAlternativePatch(text, suggestion, alternativeText) {
84
- const before = text.substring(0, suggestion.start);
85
- const after = text.substring(suggestion.end);
86
- return before + alternativeText + after;
87
- }
88
-
89
- /**
90
- * Apply ONE correction and rebase ALL remaining suggestions.
91
- * This is the primary function for individual suggestion applies.
92
- *
93
- * HIGH-1 FIX: Atomic apply + rebase in a single call.
94
- *
95
- * @param {string} text - Current text (must match suggestion offsets)
96
- * @param {Object} appliedSuggestion - The suggestion being applied
97
- * @param {string} replacementText - The text to insert (correction or alternative)
98
- * @param {Array} allSuggestions - All current suggestions (including the one being applied)
99
- * @returns {{ text: string, suggestions: Array }} Updated text and rebased suggestions
100
- */
101
- function applyAndRebase(text, appliedSuggestion, replacementText, allSuggestions) {
102
- // 1. Apply the patch to text
103
- const newText = text.substring(0, appliedSuggestion.start)
104
- + replacementText
105
- + text.substring(appliedSuggestion.end);
106
-
107
- // 2. Calculate delta: how much the text length changed
108
- const originalSpanLength = appliedSuggestion.end - appliedSuggestion.start;
109
- const delta = replacementText.length - originalSpanLength;
110
-
111
- // 3. Remove the applied suggestion and rebase remaining offsets
112
- const rebased = allSuggestions
113
- .filter((s) => s.id !== appliedSuggestion.id)
114
- .map((s) => {
115
- // Only shift suggestions that start AT or AFTER the applied patch's end.
116
- // Suggestions entirely before the patch are unaffected.
117
- // (Backend guarantees no overlapping suggestions via PatchSet.resolve_overlaps())
118
- if (s.start >= appliedSuggestion.end) {
119
- return { ...s, start: s.start + delta, end: s.end + delta };
120
- }
121
- return s;
122
- });
123
-
124
- return { text: newText, suggestions: rebased };
125
- }
126
-
127
- /**
128
- * Remove a suggestion from the list (after applying or dismissing).
129
- * Source: src/js/editor.js L520-523 (UUID-based filter)
130
- *
131
- * @param {Array} suggestions - Current suggestions array
132
- * @param {string} suggestionId - ID of suggestion to remove
133
- * @returns {Array} Filtered suggestions
134
- */
135
- function removeSuggestion(suggestions, suggestionId) {
136
- return suggestions.filter((s) => s.id !== suggestionId);
137
- }
138
-
139
- /**
140
- * Count suggestions by type.
141
- * Source: src/js/editor.js L525-527 (type counting)
142
- *
143
- * @param {Array} suggestions
144
- * @returns {{ spelling: number, grammar: number, punctuation: number }}
145
- */
146
- function countByType(suggestions) {
147
- return {
148
- spelling: suggestions.filter((s) => s.type === 'spelling').length,
149
- grammar: suggestions.filter((s) => s.type === 'grammar').length,
150
- punctuation: suggestions.filter((s) => s.type === 'punctuation').length,
151
- };
152
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/shared/bayan-renderer.js DELETED
@@ -1,149 +0,0 @@
1
- /**
2
- * Bayan Chrome Extension — Renderer
3
- *
4
- * Functions reused from: src/js/renderer.js
5
- * All functions preserve original behavior exactly.
6
- * Source line references documented per Phase 0 audit.
7
- */
8
-
9
- /**
10
- * Escapes HTML special characters to prevent XSS.
11
- * Source: src/js/renderer.js L9-18 (direct copy)
12
- *
13
- * @param {string} text - Text to escape
14
- * @returns {string} Escaped text
15
- */
16
- function escapeHtml(text) {
17
- const map = {
18
- '&': '&amp;',
19
- '<': '&lt;',
20
- '>': '&gt;',
21
- '"': '&quot;',
22
- "'": '&#039;',
23
- };
24
- return text.replace(/[&<>"']/g, (c) => map[c]);
25
- }
26
-
27
- /**
28
- * Sorts suggestions by start offset.
29
- * Source: src/js/renderer.js L25-27 (direct copy)
30
- *
31
- * @param {Array} suggestions - Array of suggestions with start/end offsets
32
- * @returns {Array} Sorted suggestions
33
- */
34
- function sortSuggestions(suggestions) {
35
- return [...suggestions].sort((a, b) => a.start - b.start);
36
- }
37
-
38
- /**
39
- * Gets CSS class for suggestion type.
40
- * Source: src/js/renderer.js L138-145 (direct copy)
41
- *
42
- * @param {string} type - Suggestion type (spelling, grammar, punctuation)
43
- * @returns {string} CSS class name
44
- */
45
- function getErrorClass(type) {
46
- const classes = {
47
- spelling: 'bayan-spelling-error',
48
- grammar: 'bayan-grammar-error',
49
- punctuation: 'bayan-punctuation-suggestion',
50
- };
51
- return classes[type] || 'bayan-spelling-error';
52
- }
53
-
54
- /**
55
- * Creates a segment tree of text ranges with their suggestions.
56
- * Handles overlapping ranges by merging them.
57
- * Source: src/js/renderer.js L36-131 (direct copy)
58
- *
59
- * @param {string} text - Original text
60
- * @param {Array} suggestions - Array of suggestions with start/end offsets
61
- * @returns {Array} Array of segments with position and suggestion info
62
- */
63
- function createSegments(text, suggestions) {
64
- const sorted = sortSuggestions(suggestions);
65
- const finalSegments = [];
66
- let segStart = 0;
67
-
68
- sorted.forEach((suggestion) => {
69
- const { start, end } = suggestion;
70
-
71
- // Add text before suggestion
72
- if (segStart < start) {
73
- finalSegments.push({
74
- type: 'text',
75
- text: text.slice(segStart, start),
76
- suggestions: [],
77
- });
78
- }
79
-
80
- // Add suggested text
81
- finalSegments.push({
82
- type: 'suggestion',
83
- text: text.slice(start, end),
84
- suggestion: suggestion,
85
- });
86
-
87
- segStart = end;
88
- });
89
-
90
- // Add remaining text
91
- if (segStart < text.length) {
92
- finalSegments.push({
93
- type: 'text',
94
- text: text.slice(segStart),
95
- suggestions: [],
96
- });
97
- }
98
-
99
- return finalSegments;
100
- }
101
-
102
- /**
103
- * Renders text with highlighted suggestions as HTML string.
104
- * Source: src/js/renderer.js L153-191 (adapted for extension CSS classes)
105
- *
106
- * @param {string} text - Original text
107
- * @param {Array} suggestions - Array of suggestions with start/end offsets
108
- * @returns {string} Safe HTML string with highlights
109
- */
110
- function renderHighlightedText(text, suggestions) {
111
- if (!text || text.length === 0) return '';
112
- if (!suggestions || suggestions.length === 0) return escapeHtml(text);
113
-
114
- const segments = createSegments(text, suggestions);
115
- let html = '';
116
-
117
- segments.forEach((segment) => {
118
- if (segment.type === 'text') {
119
- html += escapeHtml(segment.text);
120
- } else if (segment.type === 'suggestion') {
121
- const { suggestion } = segment;
122
- const errorClass = getErrorClass(suggestion.type);
123
- const escapedText = escapeHtml(segment.text);
124
- const sid = suggestion.id || '';
125
-
126
- html += `<span class="${errorClass}" data-suggestion-id="${sid}" data-original="${escapeHtml(
127
- suggestion.original
128
- )}" data-correction="${escapeHtml(
129
- suggestion.correction
130
- )}" data-type="${suggestion.type}" title="${suggestion.type}: ${escapeHtml(
131
- suggestion.correction
132
- )}">${escapedText}</span>`;
133
- }
134
- });
135
-
136
- return html;
137
- }
138
-
139
- /**
140
- * Main renderer function.
141
- * Source: src/js/renderer.js L201-204 (direct copy)
142
- *
143
- * @param {Object} input - { text, suggestions }
144
- * @returns {string} Safe HTML with highlights
145
- */
146
- function bayanRender(input) {
147
- const { text = '', suggestions = [] } = input;
148
- return renderHighlightedText(text, suggestions);
149
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/bayan-ui.js DELETED
@@ -1,99 +0,0 @@
1
- /**
2
- * Bayan Chrome Extension — UI Helpers
3
- *
4
- * Functions reused from: src/js/ui.js
5
- * All functions preserve original behavior exactly.
6
- * Source line references documented per Phase 0 audit.
7
- */
8
-
9
- /**
10
- * Arabic labels for suggestion types.
11
- * Source: src/js/ui.js L3-7 (direct copy)
12
- */
13
- const TYPE_LABELS = {
14
- spelling: 'إملائي',
15
- grammar: 'نحوي',
16
- punctuation: 'ترقيم',
17
- };
18
-
19
- /**
20
- * Resolve alternatives for a suggestion, falling back to [correction, original].
21
- * Source: src/js/ui.js L67-71 (direct copy)
22
- *
23
- * @param {Object} suggestion
24
- * @returns {Array<string>}
25
- */
26
- function resolveAlternatives(suggestion) {
27
- return suggestion.alternatives && suggestion.alternatives.length > 0
28
- ? suggestion.alternatives
29
- : [suggestion.correction, suggestion.original];
30
- }
31
-
32
- /**
33
- * Calculate writing score from suggestion counts.
34
- * Source: src/js/ui.js L14-17 (direct copy)
35
- *
36
- * @param {number} spelling
37
- * @param {number} grammar
38
- * @param {number} punctuation
39
- * @returns {number} Score 0-100
40
- */
41
- function calculateWritingScore(spelling, grammar, punctuation) {
42
- const score = 100 - spelling * 8 - grammar * 6 - punctuation * 3;
43
- return Math.max(0, Math.min(100, score));
44
- }
45
-
46
- /**
47
- * Build HTML for a single suggestion card.
48
- * Source: src/js/ui.js L73-100 (adapted for extension CSS classes)
49
- *
50
- * @param {Object} suggestion
51
- * @param {number} index
52
- * @returns {string} HTML string
53
- */
54
- function buildSuggestionCardHTML(suggestion, index) {
55
- const badgeClass = `bayan-badge-${suggestion.type}`;
56
- const label = TYPE_LABELS[suggestion.type] || suggestion.type;
57
- const alts = resolveAlternatives(suggestion);
58
- const suggestionId = suggestion.id || index;
59
-
60
- let altsHTML = '';
61
- alts.forEach((alt, i) => {
62
- const isKeep = alt === suggestion.original;
63
- const isMain = i === 0;
64
- const cls = isKeep
65
- ? 'bayan-alt-chip bayan-alt-chip--keep'
66
- : isMain
67
- ? 'bayan-alt-chip bayan-alt-chip--main'
68
- : 'bayan-alt-chip';
69
- const chipLabel = isKeep ? `${escapeHtml(alt)} ✓` : escapeHtml(alt);
70
- altsHTML += `<button class="${cls}" data-card-alt="${escapeHtml(alt)}" data-card-id="${suggestionId}" type="button">${chipLabel}</button>`;
71
- });
72
-
73
- return `
74
- <div class="bayan-suggestion-card" role="listitem" tabindex="0"
75
- data-suggestion-id="${suggestionId}"
76
- aria-label="${label}: ${escapeHtml(suggestion.original)} إلى ${escapeHtml(suggestion.correction)}">
77
- <span class="bayan-suggestion-badge ${badgeClass}">${label}</span>
78
- <div class="bayan-suggestion-change">
79
- <span class="bayan-suggestion-original">${escapeHtml(suggestion.original)}</span>
80
- <span class="bayan-suggestion-arrow">←</span>
81
- <span class="bayan-suggestion-correction">${escapeHtml(suggestion.correction)}</span>
82
- </div>
83
- <div class="bayan-suggestion-alts">${altsHTML}</div>
84
- </div>`;
85
- }
86
-
87
- /**
88
- * Generate score hint text.
89
- *
90
- * @param {number} score
91
- * @param {number} total - total suggestion count
92
- * @returns {string}
93
- */
94
- function getScoreHint(score, total) {
95
- if (total === 0) return 'ابدأ الكتابة لرؤية تقييمك';
96
- if (score >= 90) return 'كتابة ممتازة! استمر.';
97
- if (score >= 70) return 'جيد — راجع الاقتراحات لتحسين النص.';
98
- return 'يحتاج النص إلى بعض التحسينات.';
99
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/shared/config.js DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * Bayan — Legacy Config (popup.js / sidepanel.js compatibility)
3
- *
4
- * This file provides the CONFIG object expected by popup.js and sidepanel.js.
5
- * All values are derived from BAYAN constants (shared/constants.js) where available,
6
- * or defined here for popup/sidepanel-specific settings.
7
- *
8
- * NOTE: popup.html and sidepanel.html load constants.js BEFORE this file,
9
- * so BAYAN is available here.
10
- */
11
- const CONFIG = {
12
- API_BASE: typeof BAYAN !== 'undefined' ? BAYAN.API_BASE : 'https://bayan10-bayan-api.hf.space',
13
- MAX_ANALYZE_LENGTH: typeof BAYAN !== 'undefined' ? BAYAN.MAX_TEXT_LENGTH : 5000,
14
- MIN_ANALYZE_LENGTH: typeof BAYAN !== 'undefined' ? BAYAN.MIN_TEXT_LENGTH : 15,
15
- ANALYZE_DEBOUNCE_MS: 3000,
16
- MIN_SUMMARIZE_LENGTH: typeof BAYAN !== 'undefined' ? BAYAN.MIN_SUMMARIZE_LENGTH : 10,
17
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/shared/constants.js DELETED
@@ -1,46 +0,0 @@
1
- /**
2
- * Bayan — Shared Constants (Single Source of Truth)
3
- *
4
- * ALL shared configuration lives here.
5
- * No hardcoded API URLs, timeouts, or limits elsewhere.
6
- *
7
- * Used by: background.js, analysis-controller.js, config.js (legacy alias)
8
- */
9
- const BAYAN = {
10
- /** Backend API base URL (HuggingFace Spaces) */
11
- API_BASE: 'https://bayan10-bayan-api.hf.space',
12
-
13
- /** Network timeout for API calls (ms) */
14
- API_TIMEOUT_MS: 60000, // FIX-09: Increased from 20s to 60s for cold-start tolerance
15
-
16
- /** Max retries on network failure */
17
- MAX_RETRIES: 1,
18
-
19
- /** Cache TTL (ms) — 5 minutes */
20
- CACHE_TTL_MS: 300000,
21
-
22
- /** Max cache entries */
23
- CACHE_MAX_ENTRIES: 20,
24
-
25
- /** Max text length for analysis */
26
- MAX_TEXT_LENGTH: 5000,
27
-
28
- /** Min text length to trigger analysis */
29
- MIN_TEXT_LENGTH: 15,
30
-
31
- /** Min Arabic characters required */
32
- MIN_ARABIC_CHARS: 5,
33
-
34
- /** Min text length for summarization */
35
- MIN_SUMMARIZE_LENGTH: 10,
36
-
37
- /** Protected hosts — disable contenteditable injection */
38
- PROTECTED_HOSTS: [
39
- 'mail.google.com',
40
- 'docs.google.com',
41
- 'sheets.google.com',
42
- 'slides.google.com',
43
- 'notion.so',
44
- 'www.notion.so',
45
- ],
46
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/shared/hash.js DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * Bayan — FNV-1a Hash (Single Implementation)
3
- *
4
- * 32-bit FNV-1a hash. Fast, zero dependencies.
5
- * Used for text deduplication in cache layers.
6
- *
7
- * @param {string} str
8
- * @returns {string} Base-36 hash string
9
- */
10
- function bayanHash(str) {
11
- let h = 0x811c9dc5;
12
- for (let i = 0; i < str.length; i++) {
13
- h ^= str.charCodeAt(i);
14
- h = Math.imul(h, 0x01000193);
15
- }
16
- return (h >>> 0).toString(36);
17
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/shared/supabase.min.js DELETED
The diff for this file is too large to render. See raw diff
 
extension/sidepanel/sidepanel.css DELETED
@@ -1,623 +0,0 @@
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: #3b82f6;
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 ── */
44
- *, *::before, *::after {
45
- box-sizing: border-box;
46
- margin: 0;
47
- padding: 0;
48
- }
49
-
50
- body {
51
- width: 100%;
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
71
- ══════════════════════════════════════════════ */
72
- .sp-header {
73
- display: flex;
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;
82
- backdrop-filter: blur(12px);
83
- }
84
-
85
- .sp-header-brand {
86
- display: flex;
87
- align-items: center;
88
- gap: 8px;
89
- }
90
-
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;
98
- }
99
-
100
- .sp-header-status {
101
- display: flex;
102
- align-items: center;
103
- gap: 5px;
104
- font-size: 11px;
105
- color: var(--sp-text-muted);
106
- }
107
-
108
- .sp-status-dot {
109
- width: 7px;
110
- height: 7px;
111
- border-radius: 50%;
112
- background: var(--sp-text-muted);
113
- transition: background var(--sp-transition);
114
- }
115
- .sp-status-dot.online { background: var(--sp-success); box-shadow: 0 0 6px var(--sp-success); }
116
- .sp-status-dot.offline { background: var(--sp-error); }
117
- .sp-status-text { font-size: 10px; }
118
-
119
- /* ══════════════════════════════════════════════
120
- Tabs
121
- ══════════════════════════════════════════════ */
122
- .sp-tabs {
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
- /* ══════════════════════════════════════════════
154
- Panels
155
- ══════════════════════════════════════════════ */
156
- .sp-panel {
157
- display: none;
158
- padding: 12px 14px;
159
- animation: spFadeIn 200ms ease;
160
- }
161
- .sp-panel.active { display: block; }
162
-
163
- @keyframes spFadeIn {
164
- from { opacity: 0; transform: translateY(4px); }
165
- to { opacity: 1; transform: translateY(0); }
166
- }
167
-
168
- /* ══════════════════════════════════════════════
169
- Input
170
- ═════════��════════════════════════════════════ */
171
- .sp-input-group {
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;
186
- min-height: 80px;
187
- transition: border-color var(--sp-transition), box-shadow var(--sp-transition);
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
-
199
- .sp-meta-row {
200
- display: flex;
201
- gap: 12px;
202
- margin-top: 4px;
203
- padding: 0 2px;
204
- }
205
- .sp-char-count,
206
- .sp-word-count {
207
- font-size: 10px;
208
- color: var(--sp-text-muted);
209
- }
210
-
211
- /* ══════════════════════════════════════════════
212
- Buttons
213
- ══════════════════════════════════════════════ */
214
- .sp-actions {
215
- display: flex;
216
- gap: 8px;
217
- margin-bottom: 12px;
218
- }
219
-
220
- .sp-btn {
221
- display: inline-flex;
222
- align-items: center;
223
- justify-content: center;
224
- gap: 5px;
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;
232
- transition: all var(--sp-transition);
233
- }
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
- }
241
- .sp-btn-primary:hover {
242
- transform: translateY(-1px);
243
- box-shadow: 0 4px 14px var(--sp-primary-glow);
244
- }
245
- .sp-btn-primary:active {
246
- transform: translateY(0);
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 {
260
- display: inline-flex;
261
- align-items: center;
262
- justify-content: center;
263
- width: 28px;
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 {
306
- position: absolute;
307
- inset: 0;
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 {
351
- display: flex;
352
- align-items: center;
353
- justify-content: space-between;
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
- Highlighted text (from bayan-renderer.js)
383
- ══════════════════════════════════════════════ */
384
- .bayan-spelling-error {
385
- background: rgba(239, 68, 68, 0.15);
386
- border-bottom: 2px solid var(--sp-spelling);
387
- padding: 0 2px;
388
- border-radius: 2px;
389
- cursor: pointer;
390
- }
391
- .bayan-grammar-error {
392
- background: rgba(245, 158, 11, 0.15);
393
- border-bottom: 2px solid var(--sp-grammar);
394
- padding: 0 2px;
395
- border-radius: 2px;
396
- cursor: pointer;
397
- }
398
- .bayan-punctuation-suggestion {
399
- background: rgba(59, 130, 246, 0.15);
400
- border-bottom: 2px solid var(--sp-punctuation);
401
- padding: 0 2px;
402
- border-radius: 2px;
403
- cursor: pointer;
404
- }
405
-
406
- /* ══════════════════════════════════════════════
407
- Suggestions
408
- ══════════════════════════════════════════════ */
409
- .sp-suggestions {
410
- margin-bottom: 12px;
411
- }
412
-
413
- .sp-suggestions-header {
414
- font-size: 11px;
415
- font-weight: 700;
416
- color: var(--sp-text-secondary);
417
- margin-bottom: 6px;
418
- padding: 0 2px;
419
- }
420
-
421
- .sp-suggestions-list {
422
- display: flex;
423
- flex-direction: column;
424
- gap: 6px;
425
- }
426
-
427
- /* Suggestion card — reuses bayan-ui.js HTML classes */
428
- .bayan-suggestion-card {
429
- padding: 8px 10px;
430
- background: var(--sp-surface);
431
- border: 1px solid var(--sp-border);
432
- border-radius: var(--sp-radius-sm);
433
- transition: border-color var(--sp-transition), background var(--sp-transition);
434
- }
435
- .bayan-suggestion-card:hover {
436
- border-color: var(--sp-border-light);
437
- background: var(--sp-surface-hover);
438
- }
439
-
440
- .bayan-suggestion-badge {
441
- display: inline-block;
442
- padding: 1px 6px;
443
- border-radius: 4px;
444
- font-size: 9px;
445
- font-weight: 700;
446
- margin-bottom: 4px;
447
- }
448
- .bayan-badge-spelling { background: rgba(239, 68, 68, 0.15); color: var(--sp-spelling); }
449
- .bayan-badge-grammar { background: rgba(245, 158, 11, 0.15); color: var(--sp-grammar); }
450
- .bayan-badge-punctuation { background: rgba(59, 130, 246, 0.15); color: var(--sp-punctuation); }
451
-
452
- .bayan-suggestion-change {
453
- display: flex;
454
- align-items: center;
455
- gap: 6px;
456
- margin-bottom: 6px;
457
- font-size: 13px;
458
- }
459
-
460
- .bayan-suggestion-original {
461
- color: var(--sp-error);
462
- text-decoration: line-through;
463
- text-decoration-color: rgba(239, 68, 68, 0.4);
464
- }
465
-
466
- .bayan-suggestion-arrow {
467
- color: var(--sp-text-muted);
468
- font-size: 11px;
469
- }
470
-
471
- .bayan-suggestion-correction {
472
- color: var(--sp-success);
473
- font-weight: 600;
474
- }
475
-
476
- .bayan-suggestion-alts {
477
- display: flex;
478
- flex-wrap: wrap;
479
- gap: 4px;
480
- }
481
-
482
- .bayan-alt-chip {
483
- padding: 3px 8px;
484
- border: 1px solid var(--sp-border);
485
- border-radius: 12px;
486
- background: var(--sp-surface-hover);
487
- color: var(--sp-text);
488
- font-family: var(--sp-font-arabic);
489
- font-size: 11px;
490
- cursor: pointer;
491
- transition: all var(--sp-transition);
492
- }
493
- .bayan-alt-chip:hover {
494
- border-color: var(--sp-primary);
495
- background: var(--sp-primary-glow);
496
- }
497
- .bayan-alt-chip--main {
498
- border-color: var(--sp-primary);
499
- background: rgba(99, 102, 241, 0.1);
500
- color: var(--sp-primary-light);
501
- }
502
- .bayan-alt-chip--keep {
503
- border-color: var(--sp-border);
504
- color: var(--sp-text-muted);
505
- font-size: 10px;
506
- }
507
- .bayan-alt-chip--keep:hover {
508
- border-color: var(--sp-text-muted);
509
- background: var(--sp-surface-active);
510
- }
511
-
512
- /* ══════════════════════════════════════════════
513
- Radio group (summary length)
514
- ══════════════════════════════════════════════ */
515
- .sp-radio-group {
516
- display: flex;
517
- gap: 12px;
518
- margin-bottom: 10px;
519
- padding: 0 2px;
520
- }
521
-
522
- .sp-radio {
523
- display: flex;
524
- align-items: center;
525
- gap: 4px;
526
- font-size: 11px;
527
- color: var(--sp-text-secondary);
528
- cursor: pointer;
529
- }
530
- .sp-radio input[type="radio"] {
531
- accent-color: var(--sp-primary);
532
- width: 13px;
533
- height: 13px;
534
- }
535
-
536
- /* ══════════════════════════════════════════════
537
- Timing
538
- ══════════════════════════════════════════════ */
539
- .sp-timing {
540
- padding: 6px 10px;
541
- margin-bottom: 8px;
542
- background: var(--sp-surface);
543
- border-radius: var(--sp-radius-sm);
544
- font-size: 10px;
545
- color: var(--sp-text-muted);
546
- text-align: center;
547
- }
548
-
549
- /* ══════════════════════════════════════════════
550
- Loading overlay
551
- ═══════════════════���══════════════════════════ */
552
- .sp-loading {
553
- position: fixed;
554
- inset: 0;
555
- z-index: 200;
556
- display: flex;
557
- flex-direction: column;
558
- align-items: center;
559
- justify-content: center;
560
- gap: 12px;
561
- background: rgba(15, 15, 20, 0.85);
562
- backdrop-filter: blur(6px);
563
- color: var(--sp-text);
564
- font-size: 13px;
565
- }
566
-
567
- .sp-spinner {
568
- width: 32px;
569
- height: 32px;
570
- border: 3px solid var(--sp-border);
571
- border-top-color: var(--sp-primary);
572
- border-radius: 50%;
573
- animation: spSpin 800ms linear infinite;
574
- }
575
-
576
- @keyframes spSpin {
577
- to { transform: rotate(360deg); }
578
- }
579
-
580
- /* ══════════════════════════════════════════════
581
- Toast
582
- ══════════════════════════════════════════════ */
583
- .sp-toast {
584
- position: fixed;
585
- bottom: 16px;
586
- left: 50%;
587
- transform: translateX(-50%) translateY(60px);
588
- padding: 8px 16px;
589
- background: var(--sp-surface);
590
- border: 1px solid var(--sp-border);
591
- border-radius: 20px;
592
- color: var(--sp-text);
593
- font-size: 12px;
594
- font-weight: 600;
595
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
596
- opacity: 0;
597
- pointer-events: none;
598
- transition: all var(--sp-spring);
599
- z-index: 300;
600
- white-space: nowrap;
601
- }
602
- .sp-toast.is-visible {
603
- opacity: 1;
604
- transform: translateX(-50%) translateY(0);
605
- }
606
-
607
- /* ══════════════════════════════════════════════
608
- Stale indicator
609
- ══════════════════════════════════════════════ */
610
- .sp-stale {
611
- opacity: 0.45;
612
- pointer-events: none;
613
- position: relative;
614
- filter: grayscale(30%);
615
- transition: opacity var(--sp-transition), filter var(--sp-transition);
616
- }
617
-
618
- /* ══════════════════════════════════════════════
619
- Utilities
620
- ══════════════════════════════════════════════ */
621
- .is-hidden {
622
- display: none !important;
623
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/sidepanel/sidepanel.html DELETED
@@ -1,193 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="ar" dir="rtl">
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>
10
- <!-- ══════════════════════════════════════════════ -->
11
- <!-- Header -->
12
- <!-- ══════════════════════════════════════════════ -->
13
- <header class="sp-header">
14
- <div class="sp-header-brand">
15
- <svg class="sp-logo" width="24" height="24" viewBox="0 0 100 100" fill="none">
16
- <circle cx="50" cy="50" r="46" fill="url(#spGrad)" />
17
- <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)" />
18
- <circle cx="50" cy="42" r="4" fill="url(#spGrad)" />
19
- <defs>
20
- <linearGradient id="spGrad" x1="0" y1="0" x2="100" y2="100">
21
- <stop offset="0%" stop-color="#6366f1"/>
22
- <stop offset="100%" stop-color="#8b5cf6"/>
23
- </linearGradient>
24
- </defs>
25
- </svg>
26
- <span class="sp-header-title">بيان</span>
27
- </div>
28
- <div class="sp-header-status" id="status-indicator">
29
- <span class="sp-status-dot"></span>
30
- <span class="sp-status-text" id="status-text">جاري الاتصال...</span>
31
- </div>
32
- </header>
33
-
34
- <!-- ══════════════════════════════════════════════ -->
35
- <!-- Tab Navigation -->
36
- <!-- ══════════════════════════════════════════════ -->
37
- <nav class="sp-tabs" role="tablist">
38
- <button class="sp-tab active" role="tab" aria-selected="true" data-tab="correct" id="tab-correct">
39
- <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>
40
- تصحيح
41
- </button>
42
- <button class="sp-tab" role="tab" aria-selected="false" data-tab="summarize" id="tab-summarize">
43
- <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>
44
- تلخيص
45
- </button>
46
- </nav>
47
-
48
- <!-- ══════════════════════════════════════════════ -->
49
- <!-- Correct Panel -->
50
- <!-- ══════════════════════════════════════════════ -->
51
- <section class="sp-panel active" id="panel-correct" role="tabpanel">
52
- <!-- Input area -->
53
- <div class="sp-input-group">
54
- <textarea
55
- id="input-text"
56
- class="sp-textarea"
57
- dir="rtl"
58
- placeholder="أدخل النص العربي أو اختر نصاً من أي صفحة..."
59
- rows="4"
60
- maxlength="5000"
61
- ></textarea>
62
- <div class="sp-meta-row">
63
- <span class="sp-char-count"><span id="char-count">٠</span> حرف</span>
64
- <span class="sp-word-count"><span id="word-count">٠</span> كلمة</span>
65
- </div>
66
- </div>
67
-
68
- <!-- Action buttons -->
69
- <div class="sp-actions">
70
- <button class="sp-btn sp-btn-primary" id="btn-correct">
71
- <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>
72
- تحليل وتصحيح
73
- </button>
74
- <button class="sp-btn sp-btn-ghost" id="btn-clear">مسح</button>
75
- </div>
76
-
77
- <!-- Score ring -->
78
- <div class="sp-score is-hidden" id="score-section">
79
- <div class="sp-score-ring">
80
- <svg viewBox="0 0 160 160">
81
- <circle cx="80" cy="80" r="70" fill="none" stroke="var(--sp-border)" stroke-width="8" />
82
- <circle cx="80" cy="80" r="70" fill="none" stroke="var(--sp-primary)" stroke-width="8"
83
- stroke-dasharray="440" stroke-dashoffset="440" stroke-linecap="round"
84
- id="score-circle" transform="rotate(-90 80 80)" />
85
- </svg>
86
- <span class="sp-score-value" id="score-value">--</span>
87
- </div>
88
- <div class="sp-score-details">
89
- <span class="sp-score-hint" id="score-hint"></span>
90
- <div class="sp-score-counts">
91
- <span class="sp-count-spelling">إملائي: <strong id="count-spelling">٠</strong></span>
92
- <span class="sp-count-grammar">نحوي: <strong id="count-grammar">٠</strong></span>
93
- <span class="sp-count-punctuation">ترقيم: <strong id="count-punctuation">٠</strong></span>
94
- </div>
95
- </div>
96
- </div>
97
-
98
- <!-- Corrected text result -->
99
- <div class="sp-result is-hidden" id="result-section">
100
- <div class="sp-result-header">
101
- <span>النص المصحح</span>
102
- <div class="sp-result-actions">
103
- <button class="sp-btn-icon" id="btn-apply-all" title="تطبيق الكل">
104
- <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>
105
- </button>
106
- <button class="sp-btn-icon" id="btn-copy-result" title="نسخ">
107
- <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>
108
- </button>
109
- </div>
110
- </div>
111
- <div class="sp-result-text" id="result-text" dir="rtl"></div>
112
- </div>
113
-
114
- <!-- Suggestions list -->
115
- <div class="sp-suggestions is-hidden" id="suggestions-section">
116
- <div class="sp-suggestions-header">الاقتراحات</div>
117
- <div class="sp-suggestions-list" id="suggestions-list" role="list"></div>
118
- </div>
119
-
120
- <!-- Timing info -->
121
- <div class="sp-timing is-hidden" id="timing-section">
122
- <span id="timing-text"></span>
123
- </div>
124
- </section>
125
-
126
- <!-- ══════════════════════════════════════════════ -->
127
- <!-- Summarize Panel -->
128
- <!-- ══════════════════════════════════════════════ -->
129
- <section class="sp-panel" id="panel-summarize" role="tabpanel">
130
- <div class="sp-input-group">
131
- <textarea
132
- id="summary-input-text"
133
- class="sp-textarea"
134
- dir="rtl"
135
- placeholder="الصق النص المراد تلخيصه..."
136
- rows="4"
137
- maxlength="5000"
138
- ></textarea>
139
- <div class="sp-meta-row">
140
- <span class="sp-char-count"><span id="summary-char-count">٠</span> حرف</span>
141
- </div>
142
- </div>
143
-
144
- <!-- Summary length options -->
145
- <div class="sp-radio-group">
146
- <label class="sp-radio"><input type="radio" name="summary-length" value="1"> قصير</label>
147
- <label class="sp-radio"><input type="radio" name="summary-length" value="2" checked> متوسط</label>
148
- <label class="sp-radio"><input type="radio" name="summary-length" value="3"> طويل</label>
149
- </div>
150
-
151
- <div class="sp-actions">
152
- <button class="sp-btn sp-btn-primary" id="btn-summarize">
153
- <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>
154
- تلخيص
155
- </button>
156
- </div>
157
-
158
- <!-- Summary result -->
159
- <div class="sp-result is-hidden" id="summary-result-section">
160
- <div class="sp-result-header">
161
- <span>الملخص</span>
162
- <button class="sp-btn-icon" id="btn-copy-summary" title="نسخ">
163
- <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>
164
- </button>
165
- </div>
166
- <div class="sp-result-text" id="summary-text" dir="rtl"></div>
167
- <div class="sp-summary-meta" id="summary-meta"></div>
168
- </div>
169
- </section>
170
-
171
- <!-- ══════════════════════════════════════════════ -->
172
- <!-- Loading Overlay -->
173
- <!-- ══════════════════════════════════════════════ -->
174
- <div class="sp-loading is-hidden" id="loading-overlay">
175
- <div class="sp-spinner"></div>
176
- <span id="loading-text">جارٍ التحليل...</span>
177
- </div>
178
-
179
- <!-- Toast -->
180
- <div class="sp-toast" id="toast"></div>
181
-
182
- <!-- ══════════════════════════════════════════════ -->
183
- <!-- Scripts — reuse ALL shared modules -->
184
- <!-- ══════════════════════════════════════════════ -->
185
- <script src="../shared/constants.js"></script>
186
- <script src="../shared/config.js"></script>
187
- <script src="../shared/bayan-renderer.js"></script>
188
- <script src="../shared/bayan-ui.js"></script>
189
- <script src="../shared/bayan-patches.js"></script>
190
- <script src="../shared/bayan-api.js"></script>
191
- <script src="sidepanel.js"></script>
192
- </body>
193
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/sidepanel/sidepanel.js DELETED
@@ -1,532 +0,0 @@
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
-
19
- document.addEventListener('DOMContentLoaded', () => {
20
- // ── Tab constants ──
21
- const TAB = { CORRECT: 'correct', SUMMARIZE: 'summarize' };
22
-
23
- // ── Element references ──
24
- const inputText = document.getElementById('input-text');
25
- const charCount = document.getElementById('char-count');
26
- const wordCount = document.getElementById('word-count');
27
- const btnCorrect = document.getElementById('btn-correct');
28
- const btnClear = document.getElementById('btn-clear');
29
- const btnApplyAll = document.getElementById('btn-apply-all');
30
- const btnCopyResult = document.getElementById('btn-copy-result');
31
- const scoreSection = document.getElementById('score-section');
32
- const resultSection = document.getElementById('result-section');
33
- const resultText = document.getElementById('result-text');
34
- const suggestionsSection = document.getElementById('suggestions-section');
35
- const suggestionsList = document.getElementById('suggestions-list');
36
- const timingSection = document.getElementById('timing-section');
37
- const timingText = document.getElementById('timing-text');
38
- const loadingOverlay = document.getElementById('loading-overlay');
39
- const loadingTextEl = document.getElementById('loading-text');
40
-
41
- // Summary tab
42
- const summaryInputText = document.getElementById('summary-input-text');
43
- const summaryCharCount = document.getElementById('summary-char-count');
44
- const btnSummarize = document.getElementById('btn-summarize');
45
- const summaryResultSection = document.getElementById('summary-result-section');
46
- const summaryText = document.getElementById('summary-text');
47
- const summaryMeta = document.getElementById('summary-meta');
48
- const btnCopySummary = document.getElementById('btn-copy-summary');
49
-
50
- // Score
51
- const scoreValue = document.getElementById('score-value');
52
- const scoreCircle = document.getElementById('score-circle');
53
- const scoreHint = document.getElementById('score-hint');
54
- const countSpelling = document.getElementById('count-spelling');
55
- const countGrammar = document.getElementById('count-grammar');
56
- const countPunctuation = document.getElementById('count-punctuation');
57
-
58
- // ══════════════════════════════════════════════════════════
59
- // State
60
- // ══════════════════════════════════════════════════════════
61
- let currentSuggestions = [];
62
- let analyzedText = '';
63
- let isStale = false;
64
- let isAnalyzing = false;
65
- let contextConsumed = false;
66
- let debounceTimer = null;
67
-
68
- const SCORE_CIRCUMFERENCE = 440;
69
- const DEBOUNCE_MS = 500;
70
-
71
- // ══════════════════════════════════════════════════════════
72
- // Tab switching
73
- // ══════════════════════════════════════════════════════════
74
- document.querySelectorAll('.sp-tab').forEach((tab) => {
75
- tab.addEventListener('click', () => {
76
- const targetTab = tab.dataset.tab;
77
- document.querySelectorAll('.sp-tab').forEach((t) => {
78
- t.classList.toggle('active', t.dataset.tab === targetTab);
79
- t.setAttribute('aria-selected', t.dataset.tab === targetTab ? 'true' : 'false');
80
- });
81
- document.querySelectorAll('.sp-panel').forEach((p) => {
82
- p.classList.toggle('active', p.id === `panel-${targetTab}`);
83
- });
84
- });
85
- });
86
-
87
- // ══════════════════════════════════════════════════════════
88
- // Character & word counter
89
- // ══════════════════════════════════════════════════════════
90
- function updateCounts(textarea, charEl, wordEl) {
91
- const text = textarea.value;
92
- const chars = text.length;
93
- const words = text.trim() ? text.trim().split(/\s+/).length : 0;
94
- if (charEl) charEl.textContent = chars.toLocaleString('ar-EG');
95
- if (wordEl) wordEl.textContent = words.toLocaleString('ar-EG');
96
- }
97
-
98
- inputText.addEventListener('input', () => {
99
- updateCounts(inputText, charCount, wordCount);
100
-
101
- // Staleness detection
102
- if (currentSuggestions.length > 0 && inputText.value !== analyzedText) {
103
- markStale();
104
- }
105
-
106
- // Debounced live re-analysis
107
- if (debounceTimer) clearTimeout(debounceTimer);
108
- debounceTimer = setTimeout(() => {
109
- const text = inputText.value.trim();
110
- if (text.length >= CONFIG.MIN_ANALYZE_LENGTH && !isAnalyzing) {
111
- runAnalysis(text);
112
- }
113
- }, DEBOUNCE_MS);
114
- });
115
-
116
- summaryInputText.addEventListener('input', () => {
117
- updateCounts(summaryInputText, summaryCharCount, null);
118
- });
119
-
120
- // ══════════════════════════════════════════════════════════
121
- // Staleness management
122
- // ══════════════════════════════════════════════════════════
123
- function markStale() {
124
- if (isStale) return;
125
- isStale = true;
126
- if (resultSection) resultSection.classList.add('sp-stale');
127
- if (suggestionsSection) suggestionsSection.classList.add('sp-stale');
128
- }
129
-
130
- function clearStale() {
131
- isStale = false;
132
- if (resultSection) resultSection.classList.remove('sp-stale');
133
- if (suggestionsSection) suggestionsSection.classList.remove('sp-stale');
134
- }
135
-
136
- // ══════════════════════════════════════════════════════════
137
- // Loading & Toast
138
- // ══════════════════════════════════════════════════════════
139
- function setLoading(show, text = 'جارٍ التحليل...') {
140
- loadingOverlay.classList.toggle('is-hidden', !show);
141
- loadingTextEl.textContent = text;
142
- }
143
-
144
- function showToast(message, duration = 2500) {
145
- const toast = document.getElementById('toast');
146
- toast.textContent = message;
147
- toast.classList.add('is-visible');
148
- clearTimeout(toast._timer);
149
- toast._timer = setTimeout(() => toast.classList.remove('is-visible'), duration);
150
- }
151
-
152
- // ══════════════════════════════════════════════════════════
153
- // Score ring
154
- // ══════════════════════════════════════════════════════════
155
- function updateScore(spelling, grammar, punctuation) {
156
- const score = calculateWritingScore(spelling, grammar, punctuation);
157
- const total = spelling + grammar + punctuation;
158
-
159
- scoreSection.classList.remove('is-hidden');
160
-
161
- if (scoreValue) scoreValue.textContent = score > 0 || total > 0 ? score.toLocaleString('ar-EG') : '--';
162
- if (scoreCircle) {
163
- const offset = SCORE_CIRCUMFERENCE - (score / 100) * SCORE_CIRCUMFERENCE;
164
- scoreCircle.style.strokeDashoffset = String(offset);
165
- }
166
- if (scoreHint) scoreHint.textContent = getScoreHint(score, total);
167
- if (countSpelling) countSpelling.textContent = spelling.toLocaleString('ar-EG');
168
- if (countGrammar) countGrammar.textContent = grammar.toLocaleString('ar-EG');
169
- if (countPunctuation) countPunctuation.textContent = punctuation.toLocaleString('ar-EG');
170
- }
171
-
172
- // ══════════════════════════════════════════════════════════
173
- // Render suggestions list
174
- // ══════════════════════════════════════════════════════════
175
- function renderSuggestions(suggestions) {
176
- currentSuggestions = suggestions;
177
-
178
- if (!suggestions || suggestions.length === 0) {
179
- suggestionsSection.classList.add('is-hidden');
180
- btnApplyAll.classList.add('is-hidden');
181
- return;
182
- }
183
-
184
- suggestionsSection.classList.remove('is-hidden');
185
- suggestionsList.innerHTML = suggestions.map((s, i) => buildSuggestionCardHTML(s, i)).join('');
186
-
187
- // Bind alt-chip click events
188
- suggestionsList.querySelectorAll('.bayan-alt-chip').forEach((chip) => {
189
- chip.addEventListener('click', (e) => {
190
- e.stopPropagation();
191
-
192
- if (isStale) {
193
- showToast('⚠ أعد التحليل أولاً — النص تغيّر');
194
- return;
195
- }
196
-
197
- const suggestionId = chip.dataset.cardId;
198
- const altText = chip.dataset.cardAlt;
199
- const suggestion = currentSuggestions.find((s) => String(s.id || '') === String(suggestionId));
200
- if (!suggestion) return;
201
-
202
- if (altText === suggestion.original) {
203
- currentSuggestions = removeSuggestion(currentSuggestions, suggestion.id);
204
- } else {
205
- const result = applyAndRebase(analyzedText, suggestion, altText, currentSuggestions);
206
- analyzedText = result.text;
207
- currentSuggestions = result.suggestions;
208
- inputText.value = analyzedText;
209
- updateCounts(inputText, charCount, wordCount);
210
- }
211
-
212
- const counts = countByType(currentSuggestions);
213
- updateScore(counts.spelling, counts.grammar, counts.punctuation);
214
- renderSuggestions(currentSuggestions);
215
- resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
216
- saveState();
217
- showToast('✓ تم التصحيح');
218
- });
219
- });
220
-
221
- btnApplyAll.classList.toggle('is-hidden', suggestions.length < 2);
222
- }
223
-
224
- // ══════════════════════════════════════════════════════════
225
- // Core analysis function
226
- // ══════════════════════════════════════════════════════════
227
- async function runAnalysis(text) {
228
- if (!text || text.length < CONFIG.MIN_ANALYZE_LENGTH) {
229
- showToast('النص قصير جداً (الحد الأدنى ١٥ حرفاً)');
230
- return;
231
- }
232
- if (text.length > CONFIG.MAX_ANALYZE_LENGTH) {
233
- showToast('النص طويل جداً (الحد الأقصى ٥٠٠٠ حرف)');
234
- return;
235
- }
236
-
237
- isAnalyzing = true;
238
- setLoading(true, 'جارٍ التحليل...');
239
- clearStale();
240
-
241
- try {
242
- const data = await bayanAnalyze(text);
243
-
244
- if (data.status === 'success' || data.status === 'partial') {
245
- const suggestions = sortSuggestions(data.suggestions || []);
246
- currentSuggestions = suggestions;
247
- analyzedText = data.original;
248
-
249
- inputText.value = analyzedText;
250
- updateCounts(inputText, charCount, wordCount);
251
-
252
- resultSection.classList.remove('is-hidden');
253
- resultText.innerHTML = renderHighlightedText(analyzedText, suggestions);
254
-
255
- const counts = countByType(suggestions);
256
- updateScore(counts.spelling, counts.grammar, counts.punctuation);
257
- renderSuggestions(suggestions);
258
-
259
- if (data.timing_ms) {
260
- timingSection.classList.remove('is-hidden');
261
- timingText.textContent = `التحليل: ${data.timing_ms.total_ms || 0}ms`;
262
- }
263
-
264
- saveState();
265
-
266
- if (suggestions.length === 0) {
267
- showToast('نصك ممتاز! لم نجد أي أخطاء ✨');
268
- }
269
- } else {
270
- showToast('تعذّر التحليل — حاول مرة أخرى');
271
- }
272
- } catch (error) {
273
- console.error('[Bayan SP] Analysis error:', error);
274
- showToast('خطأ في الاتصال — تحقق من الإنترنت');
275
- } finally {
276
- isAnalyzing = false;
277
- setLoading(false);
278
- }
279
- }
280
-
281
- // ══════════════════════════════════════════════════════════
282
- // State persistence (side panel survives navigation)
283
- // ══════════════════════════════════════════════════════════
284
- function getStorage() {
285
- return chrome.storage?.session || chrome.storage?.local;
286
- }
287
-
288
- function saveState() {
289
- const storage = getStorage();
290
- if (!storage) return;
291
- storage.set({
292
- spLastText: analyzedText,
293
- spLastSuggestions: currentSuggestions,
294
- spLastInput: inputText.value,
295
- }).catch(() => {});
296
- }
297
-
298
- async function restoreState() {
299
- const storage = getStorage();
300
- if (!storage) return false;
301
-
302
- try {
303
- const data = await storage.get(['spLastText', 'spLastSuggestions', 'spLastInput']);
304
- if (!data.spLastText || !data.spLastSuggestions) return false;
305
-
306
- analyzedText = data.spLastText;
307
- currentSuggestions = data.spLastSuggestions;
308
- inputText.value = data.spLastInput || analyzedText;
309
- updateCounts(inputText, charCount, wordCount);
310
-
311
- resultSection.classList.remove('is-hidden');
312
- resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
313
-
314
- const counts = countByType(currentSuggestions);
315
- updateScore(counts.spelling, counts.grammar, counts.punctuation);
316
- renderSuggestions(currentSuggestions);
317
-
318
- return true;
319
- } catch {
320
- return false;
321
- }
322
- }
323
-
324
- // ══════════════════════════════════════════════════════════
325
- // Button handlers
326
- // ═══════════════════════════════════════════════════════���══
327
- btnCorrect.addEventListener('click', () => {
328
- const text = inputText.value.trim();
329
- runAnalysis(text);
330
- });
331
-
332
- btnClear.addEventListener('click', () => {
333
- inputText.value = '';
334
- analyzedText = '';
335
- updateCounts(inputText, charCount, wordCount);
336
- scoreSection.classList.add('is-hidden');
337
- resultSection.classList.add('is-hidden');
338
- suggestionsSection.classList.add('is-hidden');
339
- timingSection.classList.add('is-hidden');
340
- currentSuggestions = [];
341
- clearStale();
342
- const storage = getStorage();
343
- if (storage) storage.remove(['spLastText', 'spLastSuggestions', 'spLastInput']).catch(() => {});
344
- });
345
-
346
- btnApplyAll.addEventListener('click', () => {
347
- if (currentSuggestions.length === 0) return;
348
- if (isStale) {
349
- showToast('⚠ أعد التحليل أولاً — النص تغيّر');
350
- return;
351
- }
352
-
353
- analyzedText = applyAllPatches(analyzedText, currentSuggestions);
354
- inputText.value = analyzedText;
355
- updateCounts(inputText, charCount, wordCount);
356
- currentSuggestions = [];
357
- resultText.innerHTML = escapeHtml(analyzedText);
358
- updateScore(0, 0, 0);
359
- renderSuggestions([]);
360
- saveState();
361
- showToast('✓ تم تطبيق جميع التصحيحات');
362
- });
363
-
364
- btnCopyResult.addEventListener('click', () => {
365
- const text = resultText.textContent || '';
366
- navigator.clipboard.writeText(text)
367
- .then(() => showToast('✓ تم نسخ النص'))
368
- .catch(() => showToast('تعذّر النسخ'));
369
- });
370
-
371
- // ══════════════════════════════════════════════════════════
372
- // Summarize
373
- // ══════════════════════════════════════════════════════════
374
- btnSummarize.addEventListener('click', async () => {
375
- const text = summaryInputText.value.trim();
376
- if (!text) { showToast('أدخل نصاً للتلخيص'); return; }
377
- if (text.length < CONFIG.MIN_SUMMARIZE_LENGTH) { showToast('النص قصير جداً للتلخيص'); return; }
378
-
379
- const lengthValue = parseInt(document.querySelector('input[name="summary-length"]:checked')?.value || '2', 10);
380
- setLoading(true, 'جارٍ التلخيص...');
381
-
382
- try {
383
- const data = await bayanSummarize(text, lengthValue);
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('تعذّر التلخيص — حاول مرة أخرى');
391
- }
392
- } catch (error) {
393
- console.error('[Bayan SP] Summarization error:', error);
394
- showToast('خطأ في الاتصال — تحقق من الإنترنت');
395
- } finally {
396
- setLoading(false);
397
- }
398
- });
399
-
400
- btnCopySummary.addEventListener('click', () => {
401
- const text = summaryText.textContent || '';
402
- navigator.clipboard.writeText(text)
403
- .then(() => showToast('✓ تم نسخ الملخص'))
404
- .catch(() => showToast('تعذّر النسخ'));
405
- });
406
-
407
- // ══════════════════════════════════════════════════════════
408
- // Status check
409
- // ══════════════════════════════════════════════════════════
410
- (async function checkStatus() {
411
- const statusDot = document.querySelector('.sp-status-dot');
412
- const statusText = document.getElementById('status-text');
413
- try {
414
- await bayanHealthCheck();
415
- if (statusDot) statusDot.classList.add('online');
416
- if (statusText) statusText.textContent = 'متصل';
417
- } catch {
418
- if (statusDot) statusDot.classList.add('offline');
419
- if (statusText) statusText.textContent = 'غير متصل';
420
- }
421
- })();
422
-
423
- // ══════════════════════════════════════════════════════════
424
- // Context menu pickup (from background.js → storage)
425
- // NOTE: background.js calls sidePanel.open() BEFORE storage.set()
426
- // to preserve the user gesture token. This means on first open,
427
- // storage may not be ready yet. We retry once after 300ms.
428
- // If the panel is ALREADY open, the storage.onChanged listener
429
- // below catches new actions in real-time.
430
- // ══════════════════════════════════════════════════════════
431
- async function tryPickupContext(retryCount = 0) {
432
- if (typeof chrome === 'undefined' || !chrome.storage) return;
433
- if (contextConsumed) return;
434
-
435
- const storage = getStorage();
436
- if (!storage) return;
437
-
438
- try {
439
- const data = await storage.get(['contextAction', 'contextText', 'contextTimestamp']);
440
-
441
- // Storage not ready yet — retry once after 300ms
442
- if ((!data.contextAction || !data.contextText) && retryCount < 2) {
443
- setTimeout(() => tryPickupContext(retryCount + 1), 300);
444
- return;
445
- }
446
-
447
- if (!data.contextAction || !data.contextText) {
448
- // No context action after retries — restore previous state
449
- await restoreState();
450
- return;
451
- }
452
-
453
- const age = Date.now() - (data.contextTimestamp || 0);
454
- if (age > 15000) {
455
- chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
456
- await restoreState();
457
- return;
458
- }
459
-
460
- contextConsumed = true;
461
-
462
- console.log(`[Bayan SP] Context action: ${data.contextAction}, text: ${data.contextText.length} chars`);
463
-
464
- if (data.contextAction === TAB.CORRECT) {
465
- inputText.value = data.contextText;
466
- updateCounts(inputText, charCount, wordCount);
467
-
468
- const correctTab = document.querySelector(`[data-tab="${TAB.CORRECT}"]`);
469
- if (correctTab) correctTab.click();
470
-
471
- // Auto-analyze immediately
472
- setTimeout(() => runAnalysis(data.contextText), 100);
473
-
474
- } else if (data.contextAction === TAB.SUMMARIZE) {
475
- summaryInputText.value = data.contextText;
476
- updateCounts(summaryInputText, summaryCharCount, null);
477
-
478
- const summarizeTab = document.querySelector(`[data-tab="${TAB.SUMMARIZE}"]`);
479
- if (summarizeTab) summarizeTab.click();
480
-
481
- setTimeout(() => btnSummarize.click(), 100);
482
- }
483
-
484
- chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
485
-
486
- } catch (err) {
487
- console.warn('[Bayan SP] Context action check failed:', err);
488
- await restoreState();
489
- }
490
- }
491
-
492
- // Start context pickup with retry
493
- tryPickupContext(0);
494
-
495
- // ══════════════════════════════════════════════════════════
496
- // Listen for NEW context actions while panel is already open
497
- // ══════════════════════════════════════════════════════════
498
- if (chrome.storage?.session?.onChanged || chrome.storage?.onChanged) {
499
- const storageApi = chrome.storage;
500
- storageApi.onChanged.addListener((changes, area) => {
501
- if (area !== 'session' && area !== 'local') return;
502
- if (!changes.contextAction || !changes.contextText) return;
503
-
504
- const action = changes.contextAction.newValue;
505
- const text = changes.contextText.newValue;
506
- if (!action || !text) return;
507
-
508
- console.log(`[Bayan SP] Storage changed — new context: ${action}, ${text.length} chars`);
509
-
510
- if (action === TAB.CORRECT) {
511
- inputText.value = text;
512
- updateCounts(inputText, charCount, wordCount);
513
-
514
- const correctTab = document.querySelector(`[data-tab="${TAB.CORRECT}"]`);
515
- if (correctTab) correctTab.click();
516
-
517
- runAnalysis(text);
518
-
519
- } else if (action === TAB.SUMMARIZE) {
520
- summaryInputText.value = text;
521
- updateCounts(summaryInputText, summaryCharCount, null);
522
-
523
- const summarizeTab = document.querySelector(`[data-tab="${TAB.SUMMARIZE}"]`);
524
- if (summarizeTab) summarizeTab.click();
525
-
526
- setTimeout(() => btnSummarize.click(), 100);
527
- }
528
-
529
- chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
530
- });
531
- }
532
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- }