Mohamed Atef commited on
Commit ·
f8feb00
1
Parent(s): 74411f9
Change 1 — Write-back to the page field
Browse filesChange 2 — Context menu items
Change 3 — Suppress re-analysis after non-correction write-back
extension/PLAN_apply_contextmenu_reanalysis.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# BAYAN Extension — Implementation Plan: Apply-to-Field, Context Menu Features & Smart Re-Analysis
|
| 2 |
+
|
| 3 |
+
> **For:** the coding agent implementing these changes.
|
| 4 |
+
> **Scope:** Chrome extension only (`extension/`). No backend changes.
|
| 5 |
+
> **Read this whole file before editing.** Each change lists the exact files, the current behavior, the target behavior, and the wiring required. Implement the three changes in order — Change 1 builds the messaging channel that Change 3 reuses.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## Background: how the pieces currently connect
|
| 10 |
+
|
| 11 |
+
- **`content-inline.js`** — injected into every page. Detects editable fields (`textarea`, `input`, `contenteditable`), analyzes them, and renders an overlay + tooltip. It **already writes corrections back** to the page field via `applyFix()`. It currently has **no `chrome.runtime.onMessage` listener** (it only *sends* messages).
|
| 12 |
+
- **`background.js`** — service worker. Owns the context menu and the API bridge. Registers exactly two menu items: `bayan-correct`, `bayan-summarize`. On click it opens the side panel and stashes `{contextAction, contextText}` in `chrome.storage.session`.
|
| 13 |
+
- **`sidepanel/sidepanel.js`** — reads that stashed context on open, switches to the matching tab, and auto-runs. It has `correct`, `summarize`, `dialect`, `quran`, `autocomplete` tabs and handlers. Its apply / apply-all buttons currently **only edit the side panel's own textarea** — they do **not** touch the page field.
|
| 14 |
+
- **`popup.js`** — same apply logic as the side panel, also only editing its own textarea.
|
| 15 |
+
|
| 16 |
+
Key gap for Changes 1 & 3: **the panel surfaces have no link back to the page field the text came from.** We must establish that link.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## CHANGE 1 — "Apply" / "Apply all" writes the result into the user's actual text field
|
| 21 |
+
|
| 22 |
+
### Current behavior
|
| 23 |
+
- **Inline tooltip apply** (`content-inline.js` → `applyFix`): ✅ already writes back to the page field and dispatches an `input` event. No change needed here.
|
| 24 |
+
- **Side panel & popup apply / apply-all**: ❌ only mutate the panel's own `<textarea id="input-text">`. The user's text field on the page is untouched.
|
| 25 |
+
|
| 26 |
+
### Target behavior
|
| 27 |
+
When the user clicks **Apply** (single suggestion) or **Apply all** in the **side panel** (and popup where applicable), the corrected text is written into the **original page field** that the text came from — replacing the selection, or the whole field if appropriate — and an `input` event is dispatched so the host page registers the change.
|
| 28 |
+
|
| 29 |
+
### Why this needs a messaging channel
|
| 30 |
+
The side panel is a separate document; it cannot touch the page DOM directly. It must send a message → background → content script → content script writes into the field.
|
| 31 |
+
|
| 32 |
+
### Implementation
|
| 33 |
+
|
| 34 |
+
**1.1 — `content-inline.js`: track the "source field" and expose a write-back handler**
|
| 35 |
+
|
| 36 |
+
- Add module state: `let lastInteractedField = null;`
|
| 37 |
+
- In `attachField(field)` (or `focusin`), set `lastInteractedField = field;` whenever a real editable field is focused. (Keep `activeField` semantics as-is; `lastInteractedField` persists even after focus moves to the side panel, which `activeField`/`detachField` would clear.)
|
| 38 |
+
- When the FAB sends `OPEN_SIDEPANEL` (existing code ~line 548), also include a stable identifier so we can re-find the field. Simplest robust approach: **tag the field** with a data attribute when opening the panel:
|
| 39 |
+
```js
|
| 40 |
+
// before sending OPEN_SIDEPANEL
|
| 41 |
+
if (lastInteractedField) lastInteractedField.dataset.bayanSource = '1';
|
| 42 |
+
```
|
| 43 |
+
- Add a **`chrome.runtime.onMessage` listener** (the content script currently has none):
|
| 44 |
+
```js
|
| 45 |
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
| 46 |
+
if (msg.type === 'BAYAN_WRITE_BACK') {
|
| 47 |
+
const field = lastInteractedField
|
| 48 |
+
|| document.querySelector('[data-bayan-source="1"]')
|
| 49 |
+
|| (isEditableField(document.activeElement) ? document.activeElement : null);
|
| 50 |
+
if (!field) { sendResponse({ ok: false, reason: 'no_field' }); return true; }
|
| 51 |
+
writeTextToField(field, msg.text, msg.mode); // mode: 'replaceAll' | 'replaceSelection'
|
| 52 |
+
sendResponse({ ok: true });
|
| 53 |
+
return true;
|
| 54 |
+
}
|
| 55 |
+
return false;
|
| 56 |
+
});
|
| 57 |
+
```
|
| 58 |
+
- Implement `writeTextToField(field, text, mode)`:
|
| 59 |
+
- For `textarea`/`input`: if `mode === 'replaceSelection'` and `selectionStart !== selectionEnd`, splice into `field.value` at the selection; else set `field.value = text`. Then `field.setSelectionRange(end, end)` and dispatch `new Event('input', { bubbles: true })`.
|
| 60 |
+
- For `contenteditable`: focus the field and use `document.execCommand('insertText', false, text)` when there's a live selection inside it; otherwise set `field.textContent = text` and dispatch `input`. (Mirror the overlay-safe approach already used in `applyFix`.)
|
| 61 |
+
- **Set the suppression flag from Change 3** right before dispatching `input` (see Change 3) when the write came from a non-correction model. For Change 1's correction apply-back, do **not** suppress — corrected text re-analyzing is harmless/expected.
|
| 62 |
+
- Clear `data-bayan-source` after writing.
|
| 63 |
+
|
| 64 |
+
**1.2 — `background.js`: relay panel → content script**
|
| 65 |
+
|
| 66 |
+
- Add a message handler branch:
|
| 67 |
+
```js
|
| 68 |
+
if (message.type === 'WRITE_BACK_TO_PAGE') {
|
| 69 |
+
// forward to the active tab's content script
|
| 70 |
+
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
| 71 |
+
const tab = tabs[0];
|
| 72 |
+
if (!tab) { sendResponse({ ok: false }); return; }
|
| 73 |
+
chrome.tabs.sendMessage(tab.id, {
|
| 74 |
+
type: 'BAYAN_WRITE_BACK', text: message.text, mode: message.mode || 'replaceAll', source: message.source
|
| 75 |
+
}, (resp) => sendResponse(resp || { ok: false }));
|
| 76 |
+
});
|
| 77 |
+
return true; // async
|
| 78 |
+
}
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
**1.3 — `sidepanel/sidepanel.js`: send write-back after apply / apply-all**
|
| 82 |
+
|
| 83 |
+
- Add a helper:
|
| 84 |
+
```js
|
| 85 |
+
function writeBackToPage(text, mode = 'replaceAll', source = 'correct') {
|
| 86 |
+
chrome.runtime.sendMessage(
|
| 87 |
+
{ type: 'WRITE_BACK_TO_PAGE', text, mode, source },
|
| 88 |
+
(resp) => {
|
| 89 |
+
if (resp && resp.ok) showToast('✓ تم تطبيق التغييرات في الصفحة');
|
| 90 |
+
else showToast('تعذّر الكتابة في الصفحة — انسخ النص يدوياً');
|
| 91 |
+
}
|
| 92 |
+
);
|
| 93 |
+
}
|
| 94 |
+
```
|
| 95 |
+
- In the existing **apply-all** handler (after `analyzedText = applyAllPatches(...)`), call `writeBackToPage(analyzedText, 'replaceAll', 'correct');`.
|
| 96 |
+
- In the single-suggestion **apply** path (after `applyAndRebase` updates `analyzedText`), call `writeBackToPage(analyzedText, 'replaceAll', 'correct');`. (Whole-field replace is simplest and avoids offset drift between the panel copy and the live field.)
|
| 97 |
+
|
| 98 |
+
**1.4 — `popup.js`** (optional, lower priority)
|
| 99 |
+
- The popup closes when it loses focus, so write-back is less reliable there. Apply the same `writeBackToPage` helper **only if** product wants it; otherwise leave the popup apply editing its own textarea and rely on copy/download. Document the decision in a comment.
|
| 100 |
+
|
| 101 |
+
### Acceptance criteria
|
| 102 |
+
- Select/focus a page `<textarea>`, open the side panel via the FAB, click **Apply all** → the page textarea content is replaced with the corrected text and the host page sees an `input` event.
|
| 103 |
+
- Works for a single **Apply** too.
|
| 104 |
+
- If the source field can't be found, the user gets a clear toast (no silent failure).
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
## CHANGE 2 — Add "لهجات" and "قرآن" to the right-click context menu
|
| 109 |
+
|
| 110 |
+
### Current behavior
|
| 111 |
+
`background.js` registers only:
|
| 112 |
+
- `bayan-correct` → "تصحيح مع بيان"
|
| 113 |
+
- `bayan-summarize` → "تلخيص مع بيان"
|
| 114 |
+
|
| 115 |
+
### Target behavior
|
| 116 |
+
The selection context menu shows **four** Bayan items:
|
| 117 |
+
- تصحيح مع بيان (existing)
|
| 118 |
+
- تلخيص مع بيان (existing)
|
| 119 |
+
- **تحويل اللهجة إلى الفصحى مع بيان** (new)
|
| 120 |
+
- **تدقيق الآية مع بيان** (new)
|
| 121 |
+
|
| 122 |
+
Clicking a new item opens the side panel, switches to the matching tab, fills the selected text, and auto-runs that model.
|
| 123 |
+
|
| 124 |
+
### Implementation
|
| 125 |
+
|
| 126 |
+
**2.1 — `background.js`**
|
| 127 |
+
- Extend the actions map:
|
| 128 |
+
```js
|
| 129 |
+
const ACTIONS = { CORRECT: 'correct', SUMMARIZE: 'summarize', DIALECT: 'dialect', QURAN: 'quran' };
|
| 130 |
+
```
|
| 131 |
+
- In `chrome.runtime.onInstalled` add two `chrome.contextMenus.create(...)` calls:
|
| 132 |
+
```js
|
| 133 |
+
chrome.contextMenus.create({ id: 'bayan-dialect',
|
| 134 |
+
title: chrome.i18n.getMessage('contextMenuDialect') || 'تحويل اللهجة إلى الفصحى مع بيان',
|
| 135 |
+
contexts: ['selection'] });
|
| 136 |
+
chrome.contextMenus.create({ id: 'bayan-quran',
|
| 137 |
+
title: chrome.i18n.getMessage('contextMenuQuran') || 'تدقيق الآية مع بيان',
|
| 138 |
+
contexts: ['selection'] });
|
| 139 |
+
```
|
| 140 |
+
- In `chrome.contextMenus.onClicked`, add routing:
|
| 141 |
+
```js
|
| 142 |
+
if (info.menuItemId === 'bayan-dialect') action = ACTIONS.DIALECT;
|
| 143 |
+
if (info.menuItemId === 'bayan-quran') action = ACTIONS.QURAN;
|
| 144 |
+
```
|
| 145 |
+
The rest of the handler (open side panel + stash context) already works generically.
|
| 146 |
+
|
| 147 |
+
**2.2 — `sidepanel/sidepanel.js`** — handle the two new context actions on pickup
|
| 148 |
+
- The side panel already reads `contextAction` and, for `correct`/`summarize`, switches tab + auto-runs. Extend **both** pickup paths (`tryPickupContext` AND the `storage.onChanged` listener) to handle the new actions:
|
| 149 |
+
```js
|
| 150 |
+
} else if (action === 'dialect') {
|
| 151 |
+
dialectInput.value = text; updateCounts(dialectInput, dialectCharCount, null);
|
| 152 |
+
document.querySelector('[data-tab="dialect"]')?.click();
|
| 153 |
+
setTimeout(() => btnDialect.click(), 120);
|
| 154 |
+
} else if (action === 'quran') {
|
| 155 |
+
quranInput.value = text; updateCounts(quranInput, quranCharCount, null);
|
| 156 |
+
document.querySelector('[data-tab="quran"]')?.click();
|
| 157 |
+
setTimeout(() => btnQuran.click(), 120);
|
| 158 |
+
}
|
| 159 |
+
```
|
| 160 |
+
(Hoist `dialectInput`, `btnDialect`, `quranInput`, `btnQuran`, etc. so they're in scope of the pickup functions, or wrap the pickup dispatch in a small `runContextAction(action, text)` function declared after all element refs.)
|
| 161 |
+
- Update the `TAB` constant if it's used for validation: `const TAB = { CORRECT:'correct', SUMMARIZE:'summarize', DIALECT:'dialect', QURAN:'quran' };`
|
| 162 |
+
|
| 163 |
+
**2.3 — Locales** — add the two new menu strings
|
| 164 |
+
- `_locales/ar/messages.json`:
|
| 165 |
+
```json
|
| 166 |
+
"contextMenuDialect": { "message": "تحويل اللهجة إلى الفصحى مع بيان", "description": "Context menu: dialect→MSA" },
|
| 167 |
+
"contextMenuQuran": { "message": "تدقيق الآية مع بيان", "description": "Context menu: Quran verify" }
|
| 168 |
+
```
|
| 169 |
+
- `_locales/en/messages.json`:
|
| 170 |
+
```json
|
| 171 |
+
"contextMenuDialect": { "message": "Convert dialect to MSA with Bayan", "description": "Context menu: dialect→MSA" },
|
| 172 |
+
"contextMenuQuran": { "message": "Verify verse with Bayan", "description": "Context menu: Quran verify" }
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
### Acceptance criteria
|
| 176 |
+
- Right-clicking selected Arabic text shows all four Bayan items.
|
| 177 |
+
- "تحويل اللهجة…" opens the side panel on the **لهجة** tab with the text filled and converted.
|
| 178 |
+
- "تدقيق الآية…" opens the side panel on the **قرآن** tab with the text filled and checked.
|
| 179 |
+
- Existing correct/summarize items are unchanged.
|
| 180 |
+
|
| 181 |
+
---
|
| 182 |
+
|
| 183 |
+
## CHANGE 3 — After applying summarize / dialect / quran output into a field, do NOT auto-run the correction model on it
|
| 184 |
+
|
| 185 |
+
### The problem
|
| 186 |
+
`content-inline.js` analyzes editable fields on every keystroke and on programmatic `input` events. If text written back into the field came from the **summarize**, **dialect**, or **quran** model (Change 1's write-back for those flows), the correction pipeline would immediately re-analyze it — which the user does **not** want (a summary/MSA/verse is the intended final text, not something to "correct").
|
| 187 |
+
|
| 188 |
+
> Note: for the **correction** apply-back (Change 1), re-analysis is fine/expected and must stay enabled.
|
| 189 |
+
|
| 190 |
+
### Target behavior
|
| 191 |
+
When a write-back originates from a **non-correction** model (`summarize` / `dialect` / `quran`), the content script must **suppress correction analysis** for that field until the **user makes a genuine manual edit** (a real keystroke), at which point normal analysis resumes.
|
| 192 |
+
|
| 193 |
+
### Implementation (`content-inline.js`)
|
| 194 |
+
|
| 195 |
+
- Add module state:
|
| 196 |
+
```js
|
| 197 |
+
let analysisSuppressed = false; // true after non-correction model write-back
|
| 198 |
+
```
|
| 199 |
+
- Extend `writeTextToField(field, text, mode)` (from Change 1) to accept the `source`/`mode` info, and:
|
| 200 |
+
- If the write-back `source` is one of `summarize|dialect|quran`, set `analysisSuppressed = true;` **before** dispatching the `input` event.
|
| 201 |
+
- If the `source` is `correct` (or inline correction apply), leave `analysisSuppressed = false`.
|
| 202 |
+
- In `onFieldInput()` (the input handler), **gate the analysis** but distinguish programmatic vs. human input. The cleanest signal: the synthetic `input` event we dispatch is not trusted. So:
|
| 203 |
+
```js
|
| 204 |
+
function onFieldInput(e) {
|
| 205 |
+
if (paused || !activeField) return;
|
| 206 |
+
|
| 207 |
+
const programmatic = e && e.isTrusted === false;
|
| 208 |
+
// A genuine user keystroke clears suppression and re-enables analysis.
|
| 209 |
+
if (!programmatic && analysisSuppressed) analysisSuppressed = false;
|
| 210 |
+
|
| 211 |
+
// Ghost-text autocomplete still runs (it's not the correction model).
|
| 212 |
+
scheduleGhost();
|
| 213 |
+
|
| 214 |
+
if (analysisSuppressed) { // model output just written — skip correction
|
| 215 |
+
clearHighlights();
|
| 216 |
+
updateBadge(0);
|
| 217 |
+
return;
|
| 218 |
+
}
|
| 219 |
+
// ...existing analysis path unchanged...
|
| 220 |
+
}
|
| 221 |
+
```
|
| 222 |
+
- Ensure `onFieldInput` is registered so it receives the event object (it's added via `field.addEventListener('input', onFieldInput)` — the handler already receives `e`).
|
| 223 |
+
- The write-back's dispatched event uses `new Event('input', {bubbles:true})`, whose `isTrusted` is `false` — this is the reliable discriminator between "we wrote this" and "the user typed."
|
| 224 |
+
- Edge cases to handle:
|
| 225 |
+
- **Badge state:** when suppressed, show the clean/✓ badge (0), not the analyzing spinner.
|
| 226 |
+
- **Decide on ghost-text:** keep autocomplete ghost active (it's a separate, opt-in feature) OR also suppress it for summarize/quran outputs if product prefers a fully "frozen" result. Default: keep ghost on; add a one-line comment noting the choice.
|
| 227 |
+
- **Re-enable correctness:** the very next real keystroke must restore analysis. Verify by typing one character after a dialect write-back → underlines should come back.
|
| 228 |
+
|
| 229 |
+
### Acceptance criteria
|
| 230 |
+
- Apply a **dialect** (or summarize/quran) result into a page field via Change 1 → the field shows the model output with **no correction underlines** and the FAB badge is not in an error/analyzing state.
|
| 231 |
+
- Type one character in that field → correction analysis resumes normally.
|
| 232 |
+
- Applying a **correction** result still re-analyzes as before (suppression must NOT trigger for `source: 'correct'`).
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
## Suggested implementation order & testing
|
| 237 |
+
|
| 238 |
+
1. **Change 2** first (self-contained, no messaging) — fastest win, easy to verify.
|
| 239 |
+
2. **Change 1** (build the messaging channel: content-script `onMessage` + `writeTextToField` + background relay + side-panel `writeBackToPage`).
|
| 240 |
+
3. **Change 3** (reuses Change 1's `writeTextToField` + `source` flag).
|
| 241 |
+
|
| 242 |
+
### Manual test pass (Chrome, `chrome://extensions` → Load unpacked → `extension/`)
|
| 243 |
+
- Reload the extension after editing the service worker / manifest (context menus only re-register on install/update — use the reload button).
|
| 244 |
+
- **Change 2:** right-click selected Arabic text → confirm 4 items → click لهجات and قرآن → correct tab opens + auto-runs.
|
| 245 |
+
- **Change 1:** focus a page `<textarea>`, open side panel via FAB, Apply / Apply all → page field updates + host page sees the change.
|
| 246 |
+
- **Change 3:** send a dialect result back to the field → no correction underlines → type one char → underlines return. Confirm a correction apply-back still re-analyzes.
|
| 247 |
+
|
| 248 |
+
### Files touched (summary)
|
| 249 |
+
| File | Change 1 | Change 2 | Change 3 |
|
| 250 |
+
|------|:---:|:---:|:---:|
|
| 251 |
+
| `extension/content-inline.js` | ✅ onMessage + `writeTextToField` + `lastInteractedField` | — | ✅ `analysisSuppressed` gate |
|
| 252 |
+
| `extension/background.js` | ✅ `WRITE_BACK_TO_PAGE` relay | ✅ 2 menu items + routing | — |
|
| 253 |
+
| `extension/sidepanel/sidepanel.js` | ✅ `writeBackToPage` on apply/apply-all | ✅ pickup for dialect/quran | ✅ pass correct `source` |
|
| 254 |
+
| `extension/popup.js` | ⚠️ optional | — | — |
|
| 255 |
+
| `extension/_locales/ar/messages.json` | — | ✅ 2 strings | — |
|
| 256 |
+
| `extension/_locales/en/messages.json` | — | ✅ 2 strings | — |
|
| 257 |
+
|
| 258 |
+
### Guardrails
|
| 259 |
+
- Don't break the existing inline `applyFix` write-back — it already works; reuse its patterns, don't replace it.
|
| 260 |
+
- All cross-document communication goes panel → `background.js` → content script. The side panel must never assume direct page DOM access.
|
| 261 |
+
- Use `event.isTrusted === false` (not a custom flag on the event) to detect programmatic input — it's tamper-proof and requires no host-page cooperation.
|
| 262 |
+
- Keep every new code path guarded (`if (!field) ...`, `resp || {ok:false}`) so a missing field or closed tab degrades to a toast, never an uncaught error.
|
extension/_locales/ar/messages.json
CHANGED
|
@@ -14,5 +14,13 @@
|
|
| 14 |
"contextMenuSummarize": {
|
| 15 |
"message": "تلخيص مع بيان",
|
| 16 |
"description": "Context menu item for summarizing selected text"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
}
|
|
|
|
| 14 |
"contextMenuSummarize": {
|
| 15 |
"message": "تلخيص مع بيان",
|
| 16 |
"description": "Context menu item for summarizing selected text"
|
| 17 |
+
},
|
| 18 |
+
"contextMenuDialect": {
|
| 19 |
+
"message": "تحويل اللهجة إلى الفصحى مع بيان",
|
| 20 |
+
"description": "Context menu: dialect→MSA"
|
| 21 |
+
},
|
| 22 |
+
"contextMenuQuran": {
|
| 23 |
+
"message": "تدقيق الآية مع بيان",
|
| 24 |
+
"description": "Context menu: Quran verify"
|
| 25 |
}
|
| 26 |
}
|
extension/_locales/en/messages.json
CHANGED
|
@@ -14,5 +14,13 @@
|
|
| 14 |
"contextMenuSummarize": {
|
| 15 |
"message": "Summarize with Bayan",
|
| 16 |
"description": "Context menu item for summarizing selected text"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
}
|
|
|
|
| 14 |
"contextMenuSummarize": {
|
| 15 |
"message": "Summarize with Bayan",
|
| 16 |
"description": "Context menu item for summarizing selected text"
|
| 17 |
+
},
|
| 18 |
+
"contextMenuDialect": {
|
| 19 |
+
"message": "Convert dialect to MSA with Bayan",
|
| 20 |
+
"description": "Context menu: dialect→MSA"
|
| 21 |
+
},
|
| 22 |
+
"contextMenuQuran": {
|
| 23 |
+
"message": "Verify verse with Bayan",
|
| 24 |
+
"description": "Context menu: Quran verify"
|
| 25 |
}
|
| 26 |
}
|
extension/background.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 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 |
|
|
@@ -59,6 +59,16 @@ chrome.runtime.onInstalled.addListener(() => {
|
|
| 59 |
title: chrome.i18n.getMessage('contextMenuSummarize') || 'تلخيص مع بيان',
|
| 60 |
contexts: ['selection'],
|
| 61 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
});
|
| 63 |
|
| 64 |
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
|
@@ -68,6 +78,8 @@ chrome.contextMenus.onClicked.addListener((info, tab) => {
|
|
| 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)
|
|
@@ -174,6 +186,21 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
| 174 |
return true;
|
| 175 |
}
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
return false;
|
| 178 |
});
|
| 179 |
|
|
|
|
| 12 |
importScripts('shared/constants.js', 'shared/hash.js');
|
| 13 |
|
| 14 |
// ── Context constants ──
|
| 15 |
+
const ACTIONS = { CORRECT: 'correct', SUMMARIZE: 'summarize', DIALECT: 'dialect', QURAN: 'quran' };
|
| 16 |
const CONTEXT_KEYS = ['contextAction', 'contextText', 'contextTimestamp'];
|
| 17 |
const SIDE_PANEL_PATH = 'sidepanel/sidepanel.html';
|
| 18 |
|
|
|
|
| 59 |
title: chrome.i18n.getMessage('contextMenuSummarize') || 'تلخيص مع بيان',
|
| 60 |
contexts: ['selection'],
|
| 61 |
});
|
| 62 |
+
chrome.contextMenus.create({
|
| 63 |
+
id: 'bayan-dialect',
|
| 64 |
+
title: chrome.i18n.getMessage('contextMenuDialect') || 'تحويل اللهجة إلى الفصحى مع بيان',
|
| 65 |
+
contexts: ['selection'],
|
| 66 |
+
});
|
| 67 |
+
chrome.contextMenus.create({
|
| 68 |
+
id: 'bayan-quran',
|
| 69 |
+
title: chrome.i18n.getMessage('contextMenuQuran') || 'تدقيق الآية مع بيان',
|
| 70 |
+
contexts: ['selection'],
|
| 71 |
+
});
|
| 72 |
});
|
| 73 |
|
| 74 |
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
|
|
|
| 78 |
let action = null;
|
| 79 |
if (info.menuItemId === 'bayan-correct') action = ACTIONS.CORRECT;
|
| 80 |
if (info.menuItemId === 'bayan-summarize') action = ACTIONS.SUMMARIZE;
|
| 81 |
+
if (info.menuItemId === 'bayan-dialect') action = ACTIONS.DIALECT;
|
| 82 |
+
if (info.menuItemId === 'bayan-quran') action = ACTIONS.QURAN;
|
| 83 |
if (!action) return;
|
| 84 |
|
| 85 |
// Open side panel IMMEDIATELY (preserve user gesture)
|
|
|
|
| 186 |
return true;
|
| 187 |
}
|
| 188 |
|
| 189 |
+
if (message.type === 'WRITE_BACK_TO_PAGE') {
|
| 190 |
+
// Forward to the active tab's content script, which owns page DOM access.
|
| 191 |
+
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
| 192 |
+
const tab = tabs[0];
|
| 193 |
+
if (!tab) { sendResponse({ ok: false }); return; }
|
| 194 |
+
chrome.tabs.sendMessage(tab.id, {
|
| 195 |
+
type: 'BAYAN_WRITE_BACK',
|
| 196 |
+
text: message.text,
|
| 197 |
+
mode: message.mode || 'replaceAll',
|
| 198 |
+
source: message.source,
|
| 199 |
+
}, (resp) => sendResponse(resp || { ok: false }));
|
| 200 |
+
});
|
| 201 |
+
return true; // async
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
return false;
|
| 205 |
});
|
| 206 |
|
extension/content-inline.js
CHANGED
|
@@ -55,6 +55,8 @@
|
|
| 55 |
const IS_PROTECTED = BayanController.isProtectedSite();
|
| 56 |
|
| 57 |
let activeField = null;
|
|
|
|
|
|
|
| 58 |
let lastAnalyzedText = '';
|
| 59 |
let suggestions = [];
|
| 60 |
let paused = false;
|
|
@@ -100,14 +102,30 @@
|
|
| 100 |
// 2. ANALYSIS (delegates to BayanController)
|
| 101 |
// ══════════════════════════════════════════════════════════
|
| 102 |
|
| 103 |
-
function onFieldInput() {
|
| 104 |
if (paused || !activeField) return;
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
const text = getFieldText(activeField);
|
| 107 |
|
| 108 |
-
// Ghost-text autocomplete (textarea/input only) runs independently of
|
|
|
|
| 109 |
scheduleGhost();
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
if (!BayanController.hasArabic(text)) {
|
| 112 |
clearHighlights();
|
| 113 |
updateBadge(0);
|
|
@@ -545,6 +563,9 @@
|
|
| 545 |
}
|
| 546 |
if (suggestions.length > 0) {
|
| 547 |
try {
|
|
|
|
|
|
|
|
|
|
| 548 |
chrome.runtime.sendMessage({ type: 'OPEN_SIDEPANEL', text: lastAnalyzedText });
|
| 549 |
} catch {}
|
| 550 |
}
|
|
@@ -598,6 +619,7 @@
|
|
| 598 |
detachField();
|
| 599 |
|
| 600 |
activeField = field;
|
|
|
|
| 601 |
suggestions = [];
|
| 602 |
if (paused) paused = false;
|
| 603 |
|
|
@@ -628,6 +650,85 @@
|
|
| 628 |
if (floatingBtn) floatingBtn.classList.remove('bayan-il-fab--visible');
|
| 629 |
}
|
| 630 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
// ══════════════════════════════════════════════════════════
|
| 632 |
// Global listeners
|
| 633 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 55 |
const IS_PROTECTED = BayanController.isProtectedSite();
|
| 56 |
|
| 57 |
let activeField = null;
|
| 58 |
+
let lastInteractedField = null; // persists after focus moves to side panel (Change 1)
|
| 59 |
+
let analysisSuppressed = false; // true after a non-correction model write-back (Change 3)
|
| 60 |
let lastAnalyzedText = '';
|
| 61 |
let suggestions = [];
|
| 62 |
let paused = false;
|
|
|
|
| 102 |
// 2. ANALYSIS (delegates to BayanController)
|
| 103 |
// ══════════════════════════════════════════════════════════
|
| 104 |
|
| 105 |
+
function onFieldInput(e) {
|
| 106 |
if (paused || !activeField) return;
|
| 107 |
|
| 108 |
+
// Only a genuine user keystroke (a trusted `input` event) clears
|
| 109 |
+
// suppression and re-enables analysis. Our own write-back dispatches an
|
| 110 |
+
// untrusted event (isTrusted === false), and internal re-runs pass no
|
| 111 |
+
// event at all — neither should count as a manual edit (Change 3).
|
| 112 |
+
const isUserKeystroke = !!e && e.isTrusted === true;
|
| 113 |
+
if (isUserKeystroke && analysisSuppressed) analysisSuppressed = false;
|
| 114 |
+
|
| 115 |
const text = getFieldText(activeField);
|
| 116 |
|
| 117 |
+
// Ghost-text autocomplete (textarea/input only) runs independently of
|
| 118 |
+
// analysis. Kept active even when suppressed (separate, opt-in feature).
|
| 119 |
scheduleGhost();
|
| 120 |
|
| 121 |
+
if (analysisSuppressed) {
|
| 122 |
+
// Model output (summarize/dialect/quran) was just written — skip the
|
| 123 |
+
// correction pipeline and show the clean badge, not the spinner.
|
| 124 |
+
clearHighlights();
|
| 125 |
+
updateBadge(0);
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
if (!BayanController.hasArabic(text)) {
|
| 130 |
clearHighlights();
|
| 131 |
updateBadge(0);
|
|
|
|
| 563 |
}
|
| 564 |
if (suggestions.length > 0) {
|
| 565 |
try {
|
| 566 |
+
// Tag the source field so write-back can re-find it after focus
|
| 567 |
+
// moves to the side panel (Change 1).
|
| 568 |
+
if (lastInteractedField) lastInteractedField.dataset.bayanSource = '1';
|
| 569 |
chrome.runtime.sendMessage({ type: 'OPEN_SIDEPANEL', text: lastAnalyzedText });
|
| 570 |
} catch {}
|
| 571 |
}
|
|
|
|
| 619 |
detachField();
|
| 620 |
|
| 621 |
activeField = field;
|
| 622 |
+
lastInteractedField = field; // persists for write-back even after the panel takes focus
|
| 623 |
suggestions = [];
|
| 624 |
if (paused) paused = false;
|
| 625 |
|
|
|
|
| 650 |
if (floatingBtn) floatingBtn.classList.remove('bayan-il-fab--visible');
|
| 651 |
}
|
| 652 |
|
| 653 |
+
// ══════════════════════════════════════════════════════════
|
| 654 |
+
// Write-back from the side panel (Change 1)
|
| 655 |
+
//
|
| 656 |
+
// The side panel cannot touch page DOM, so it relays text through
|
| 657 |
+
// background.js → here. We write into the source field and dispatch
|
| 658 |
+
// a synthetic `input` event so the host page registers the change.
|
| 659 |
+
// For non-correction sources (summarize/dialect/quran) we set the
|
| 660 |
+
// suppression flag first (Change 3) so the corrected/model output is
|
| 661 |
+
// NOT immediately re-analyzed.
|
| 662 |
+
// ══════════════════════════════════════════════════════════
|
| 663 |
+
|
| 664 |
+
const NON_CORRECTION_SOURCES = ['summarize', 'dialect', 'quran'];
|
| 665 |
+
|
| 666 |
+
function writeTextToField(field, text, mode, source) {
|
| 667 |
+
if (!field || typeof text !== 'string') return;
|
| 668 |
+
|
| 669 |
+
const tag = field.tagName.toLowerCase();
|
| 670 |
+
const suppress = NON_CORRECTION_SOURCES.includes(source);
|
| 671 |
+
|
| 672 |
+
if (tag === 'textarea' || tag === 'input') {
|
| 673 |
+
if (mode === 'replaceSelection'
|
| 674 |
+
&& typeof field.selectionStart === 'number'
|
| 675 |
+
&& field.selectionStart !== field.selectionEnd) {
|
| 676 |
+
const start = field.selectionStart;
|
| 677 |
+
const end = field.selectionEnd;
|
| 678 |
+
field.value = field.value.slice(0, start) + text + field.value.slice(end);
|
| 679 |
+
const caret = start + text.length;
|
| 680 |
+
try { field.setSelectionRange(caret, caret); } catch {}
|
| 681 |
+
} else {
|
| 682 |
+
field.value = text;
|
| 683 |
+
const caret = field.value.length;
|
| 684 |
+
try { field.setSelectionRange(caret, caret); } catch {}
|
| 685 |
+
}
|
| 686 |
+
// Set suppression BEFORE dispatching so onFieldInput sees it.
|
| 687 |
+
if (suppress) analysisSuppressed = true;
|
| 688 |
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
| 689 |
+
} else {
|
| 690 |
+
// contenteditable — mirror the overlay-safe approach used by applyFix.
|
| 691 |
+
field.focus();
|
| 692 |
+
const sel = window.getSelection();
|
| 693 |
+
const hasLiveSelection = sel && sel.rangeCount > 0 && field.contains(sel.anchorNode);
|
| 694 |
+
// NOTE: execCommand('insertText') fires a TRUSTED input event, which
|
| 695 |
+
// onFieldInput would treat as a user keystroke and clear suppression.
|
| 696 |
+
// So only take that path when NOT suppressing; suppressed writes use the
|
| 697 |
+
// textContent path, whose dispatched event is untrusted (Change 3-safe).
|
| 698 |
+
if (mode === 'replaceSelection' && hasLiveSelection && !sel.isCollapsed && !suppress) {
|
| 699 |
+
document.execCommand('insertText', false, text);
|
| 700 |
+
} else {
|
| 701 |
+
field.textContent = text;
|
| 702 |
+
if (suppress) analysisSuppressed = true;
|
| 703 |
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
| 704 |
+
}
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
// Clear the source tag now that the write succeeded.
|
| 708 |
+
try { delete field.dataset.bayanSource; } catch {}
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
| 712 |
+
if (msg && msg.type === 'BAYAN_WRITE_BACK') {
|
| 713 |
+
const field = lastInteractedField
|
| 714 |
+
|| document.querySelector('[data-bayan-source="1"]')
|
| 715 |
+
|| (isEditableField(document.activeElement) ? document.activeElement : null);
|
| 716 |
+
// With all_frames:true this listener runs in every frame. A frame that
|
| 717 |
+
// doesn't own the source field stays SILENT (return false, no response)
|
| 718 |
+
// so it can't win the response race against the frame that does.
|
| 719 |
+
if (!field) return false;
|
| 720 |
+
try {
|
| 721 |
+
writeTextToField(field, msg.text, msg.mode, msg.source);
|
| 722 |
+
sendResponse({ ok: true });
|
| 723 |
+
} catch (err) {
|
| 724 |
+
console.warn('[Bayan] Write-back error:', err.message);
|
| 725 |
+
sendResponse({ ok: false, reason: 'write_error' });
|
| 726 |
+
}
|
| 727 |
+
return true;
|
| 728 |
+
}
|
| 729 |
+
return false;
|
| 730 |
+
});
|
| 731 |
+
|
| 732 |
// ══════════════════════════════════════════════════════════
|
| 733 |
// Global listeners
|
| 734 |
// ══════════════════════════════════════════════════════════
|
extension/sidepanel/sidepanel.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
| 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');
|
|
@@ -149,6 +149,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 149 |
toast._timer = setTimeout(() => toast.classList.remove('is-visible'), duration);
|
| 150 |
}
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
// ══════════════════════════════════════════════════════════
|
| 153 |
// Score ring
|
| 154 |
// ══════════════════════════════════════════════════════════
|
|
@@ -214,6 +234,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 214 |
renderSuggestions(currentSuggestions);
|
| 215 |
resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
|
| 216 |
saveState();
|
|
|
|
| 217 |
showToast('✓ تم التصحيح');
|
| 218 |
});
|
| 219 |
});
|
|
@@ -358,6 +379,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 358 |
updateScore(0, 0, 0);
|
| 359 |
renderSuggestions([]);
|
| 360 |
saveState();
|
|
|
|
| 361 |
showToast('✓ تم تطبيق جميع التصحيحات');
|
| 362 |
});
|
| 363 |
|
|
@@ -581,6 +603,34 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 581 |
addDownloadButton(btnCopyResult, () => resultText.textContent, 'bayan-corrected.txt');
|
| 582 |
addDownloadButton(btnCopySummary, () => summaryText.textContent, 'bayan-summary.txt');
|
| 583 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
// ══════════════════════════════════════════════════════════
|
| 585 |
// Status check
|
| 586 |
// ══════════════════════════════════════════════════════════
|
|
@@ -605,6 +655,34 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 605 |
// If the panel is ALREADY open, the storage.onChanged listener
|
| 606 |
// below catches new actions in real-time.
|
| 607 |
// ══════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
async function tryPickupContext(retryCount = 0) {
|
| 609 |
if (typeof chrome === 'undefined' || !chrome.storage) return;
|
| 610 |
if (contextConsumed) return;
|
|
@@ -638,25 +716,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 638 |
|
| 639 |
console.log(`[Bayan SP] Context action: ${data.contextAction}, text: ${data.contextText.length} chars`);
|
| 640 |
|
| 641 |
-
|
| 642 |
-
inputText.value = data.contextText;
|
| 643 |
-
updateCounts(inputText, charCount, wordCount);
|
| 644 |
-
|
| 645 |
-
const correctTab = document.querySelector(`[data-tab="${TAB.CORRECT}"]`);
|
| 646 |
-
if (correctTab) correctTab.click();
|
| 647 |
-
|
| 648 |
-
// Auto-analyze immediately
|
| 649 |
-
setTimeout(() => runAnalysis(data.contextText), 100);
|
| 650 |
-
|
| 651 |
-
} else if (data.contextAction === TAB.SUMMARIZE) {
|
| 652 |
-
summaryInputText.value = data.contextText;
|
| 653 |
-
updateCounts(summaryInputText, summaryCharCount, null);
|
| 654 |
-
|
| 655 |
-
const summarizeTab = document.querySelector(`[data-tab="${TAB.SUMMARIZE}"]`);
|
| 656 |
-
if (summarizeTab) summarizeTab.click();
|
| 657 |
-
|
| 658 |
-
setTimeout(() => btnSummarize.click(), 100);
|
| 659 |
-
}
|
| 660 |
|
| 661 |
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
|
| 662 |
|
|
@@ -684,24 +744,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 684 |
|
| 685 |
console.log(`[Bayan SP] Storage changed — new context: ${action}, ${text.length} chars`);
|
| 686 |
|
| 687 |
-
|
| 688 |
-
inputText.value = text;
|
| 689 |
-
updateCounts(inputText, charCount, wordCount);
|
| 690 |
-
|
| 691 |
-
const correctTab = document.querySelector(`[data-tab="${TAB.CORRECT}"]`);
|
| 692 |
-
if (correctTab) correctTab.click();
|
| 693 |
-
|
| 694 |
-
runAnalysis(text);
|
| 695 |
-
|
| 696 |
-
} else if (action === TAB.SUMMARIZE) {
|
| 697 |
-
summaryInputText.value = text;
|
| 698 |
-
updateCounts(summaryInputText, summaryCharCount, null);
|
| 699 |
-
|
| 700 |
-
const summarizeTab = document.querySelector(`[data-tab="${TAB.SUMMARIZE}"]`);
|
| 701 |
-
if (summarizeTab) summarizeTab.click();
|
| 702 |
-
|
| 703 |
-
setTimeout(() => btnSummarize.click(), 100);
|
| 704 |
-
}
|
| 705 |
|
| 706 |
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
|
| 707 |
});
|
|
|
|
| 18 |
|
| 19 |
document.addEventListener('DOMContentLoaded', () => {
|
| 20 |
// ── Tab constants ──
|
| 21 |
+
const TAB = { CORRECT: 'correct', SUMMARIZE: 'summarize', DIALECT: 'dialect', QURAN: 'quran' };
|
| 22 |
|
| 23 |
// ── Element references ──
|
| 24 |
const inputText = document.getElementById('input-text');
|
|
|
|
| 149 |
toast._timer = setTimeout(() => toast.classList.remove('is-visible'), duration);
|
| 150 |
}
|
| 151 |
|
| 152 |
+
// ══════════════════════════════════════════════════════════
|
| 153 |
+
// Write-back to the page field (panel → background → content script)
|
| 154 |
+
// The side panel is a separate document and cannot touch page DOM
|
| 155 |
+
// directly; it relays through background.js. `source` lets the content
|
| 156 |
+
// script decide whether to re-analyze (correct) or suppress (Change 3).
|
| 157 |
+
// ══════════════════════════════════════════════════════════
|
| 158 |
+
function writeBackToPage(text, mode = 'replaceAll', source = 'correct') {
|
| 159 |
+
try {
|
| 160 |
+
chrome.runtime.sendMessage(
|
| 161 |
+
{ type: 'WRITE_BACK_TO_PAGE', text, mode, source },
|
| 162 |
+
(resp) => {
|
| 163 |
+
if (resp && resp.ok) showToast('✓ تم تطبيق التغييرات في الصفحة');
|
| 164 |
+
else showToast('تعذّر الكتابة في الصفحة — انسخ النص يدوياً');
|
| 165 |
+
}
|
| 166 |
+
);
|
| 167 |
+
} catch {
|
| 168 |
+
showToast('تعذّر الكتابة في الصفحة — انسخ النص يدوياً');
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
// ══════════════════════════════════════════════════════════
|
| 173 |
// Score ring
|
| 174 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 234 |
renderSuggestions(currentSuggestions);
|
| 235 |
resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions);
|
| 236 |
saveState();
|
| 237 |
+
writeBackToPage(analyzedText, 'replaceAll', 'correct');
|
| 238 |
showToast('✓ تم التصحيح');
|
| 239 |
});
|
| 240 |
});
|
|
|
|
| 379 |
updateScore(0, 0, 0);
|
| 380 |
renderSuggestions([]);
|
| 381 |
saveState();
|
| 382 |
+
writeBackToPage(analyzedText, 'replaceAll', 'correct');
|
| 383 |
showToast('✓ تم تطبيق جميع التصحيحات');
|
| 384 |
});
|
| 385 |
|
|
|
|
| 603 |
addDownloadButton(btnCopyResult, () => resultText.textContent, 'bayan-corrected.txt');
|
| 604 |
addDownloadButton(btnCopySummary, () => summaryText.textContent, 'bayan-summary.txt');
|
| 605 |
|
| 606 |
+
// ══════════════════════════════════════════════════════════
|
| 607 |
+
// "Apply to page" buttons for summarize / dialect / quran results.
|
| 608 |
+
// These write the model output back into the source page field via
|
| 609 |
+
// Change 1's relay, tagging the write with its `source` so the content
|
| 610 |
+
// script suppresses correction re-analysis on it (Change 3).
|
| 611 |
+
// Injected programmatically to avoid touching sidepanel.html.
|
| 612 |
+
// ══════════════════════════════════════════════════════════
|
| 613 |
+
const SP_APPLY_PAGE_ICON = '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
|
| 614 |
+
|
| 615 |
+
function addApplyToPageButton(anchorBtn, getText, source) {
|
| 616 |
+
if (!anchorBtn || !anchorBtn.parentElement) return;
|
| 617 |
+
const btn = document.createElement('button');
|
| 618 |
+
btn.className = 'sp-btn-icon';
|
| 619 |
+
btn.type = 'button';
|
| 620 |
+
btn.title = 'تطبيق في الصفحة';
|
| 621 |
+
btn.innerHTML = SP_APPLY_PAGE_ICON;
|
| 622 |
+
btn.addEventListener('click', () => {
|
| 623 |
+
const text = (getText() || '').trim();
|
| 624 |
+
if (!text) { showToast('لا يوجد نص للتطبيق'); return; }
|
| 625 |
+
writeBackToPage(text, 'replaceAll', source);
|
| 626 |
+
});
|
| 627 |
+
anchorBtn.parentElement.appendChild(btn);
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
addApplyToPageButton(btnCopySummary, () => summaryText.textContent, 'summarize');
|
| 631 |
+
if (btnCopyDialect) addApplyToPageButton(btnCopyDialect, () => dialectText.textContent, 'dialect');
|
| 632 |
+
if (btnCopyQuran) addApplyToPageButton(btnCopyQuran, () => quranText.textContent, 'quran');
|
| 633 |
+
|
| 634 |
// ══════════════════════════════════════════════════════════
|
| 635 |
// Status check
|
| 636 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 655 |
// If the panel is ALREADY open, the storage.onChanged listener
|
| 656 |
// below catches new actions in real-time.
|
| 657 |
// ══════════════════════════════════════════════════════════
|
| 658 |
+
|
| 659 |
+
// Dispatch a context action (correct/summarize/dialect/quran) by filling
|
| 660 |
+
// the matching tab's input, switching to it, and auto-running its model.
|
| 661 |
+
// Declared after all element refs so dialect/quran handles are in scope.
|
| 662 |
+
function runContextAction(action, text) {
|
| 663 |
+
if (action === TAB.CORRECT) {
|
| 664 |
+
inputText.value = text;
|
| 665 |
+
updateCounts(inputText, charCount, wordCount);
|
| 666 |
+
document.querySelector(`[data-tab="${TAB.CORRECT}"]`)?.click();
|
| 667 |
+
setTimeout(() => runAnalysis(text), 100);
|
| 668 |
+
} else if (action === TAB.SUMMARIZE) {
|
| 669 |
+
summaryInputText.value = text;
|
| 670 |
+
updateCounts(summaryInputText, summaryCharCount, null);
|
| 671 |
+
document.querySelector(`[data-tab="${TAB.SUMMARIZE}"]`)?.click();
|
| 672 |
+
setTimeout(() => btnSummarize.click(), 100);
|
| 673 |
+
} else if (action === TAB.DIALECT && dialectInput && btnDialect) {
|
| 674 |
+
dialectInput.value = text;
|
| 675 |
+
updateCounts(dialectInput, dialectCharCount, null);
|
| 676 |
+
document.querySelector(`[data-tab="${TAB.DIALECT}"]`)?.click();
|
| 677 |
+
setTimeout(() => btnDialect.click(), 120);
|
| 678 |
+
} else if (action === TAB.QURAN && quranInput && btnQuran) {
|
| 679 |
+
quranInput.value = text;
|
| 680 |
+
updateCounts(quranInput, quranCharCount, null);
|
| 681 |
+
document.querySelector(`[data-tab="${TAB.QURAN}"]`)?.click();
|
| 682 |
+
setTimeout(() => btnQuran.click(), 120);
|
| 683 |
+
}
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
async function tryPickupContext(retryCount = 0) {
|
| 687 |
if (typeof chrome === 'undefined' || !chrome.storage) return;
|
| 688 |
if (contextConsumed) return;
|
|
|
|
| 716 |
|
| 717 |
console.log(`[Bayan SP] Context action: ${data.contextAction}, text: ${data.contextText.length} chars`);
|
| 718 |
|
| 719 |
+
runContextAction(data.contextAction, data.contextText);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
|
| 721 |
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
|
| 722 |
|
|
|
|
| 744 |
|
| 745 |
console.log(`[Bayan SP] Storage changed — new context: ${action}, ${text.length} chars`);
|
| 746 |
|
| 747 |
+
runContextAction(action, text);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
|
| 749 |
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
|
| 750 |
});
|