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 +0 -18
- extension/background.js +0 -194
- extension/content-inline.css +0 -268
- extension/content-inline.js +0 -514
- extension/content.js +0 -11
- extension/manifest.json +0 -55
- extension/popup.css +0 -754
- extension/popup.html +0 -199
- extension/popup.js +0 -497
- extension/shared/analysis-controller.js +0 -146
- extension/shared/bayan-api.js +0 -65
- extension/shared/bayan-patches.js +0 -152
- extension/shared/bayan-renderer.js +0 -149
- extension/shared/bayan-state.js +0 -127
- extension/shared/bayan-ui.js +0 -99
- extension/shared/config.js +0 -17
- extension/shared/constants.js +0 -46
- extension/shared/hash.js +0 -17
- extension/shared/supabase.min.js +0 -0
- extension/sidepanel/sidepanel.css +0 -623
- extension/sidepanel/sidepanel.html +0 -193
- extension/sidepanel/sidepanel.js +0 -532
- extension/tests/api_response_tc2.json +0 -41
- extension/tests/debug_offsets.ps1 +0 -14
- extension/tests/test_api_real.ps1 +0 -80
- extension/tests/test_e2e_real.html +0 -319
- extension/tests/test_inline.html +0 -78
- extension/tests/test_patches.html +0 -282
- extension/tests/test_patches.js +0 -253
- extension/tests/test_patches.ps1 +0 -172
extension/_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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
| 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 |
-
'&': '&',
|
| 19 |
-
'<': '<',
|
| 20 |
-
'>': '>',
|
| 21 |
-
'"': '"',
|
| 22 |
-
"'": ''',
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|