bayan-api / extension /sidepanel /sidepanel.js
youssefreda9's picture
Update extension JS logic
38e9deb
Raw
History Blame Contribute Delete
47.3 kB
/**
* Bayan Chrome Extension — Side Panel Logic
*
* Persistent workspace panel — reuses shared modules.
*
* Key differences from popup.js:
* - Persistent: panel stays open across page navigations
* - Auto-analysis: text injected from context menu auto-analyzes
* - Debounced live updates: re-analyzes on user edits (500ms debounce)
* - Write-back-to-page: can apply corrections/results to the source field
* - State persistence: last analysis saved to chrome.storage.session
*/
document.addEventListener('DOMContentLoaded', () => {
// ── Tab constants ──
const TAB = { CORRECT: 'correct', SUMMARIZE: 'summarize', DIALECT: 'dialect', QURAN: 'quran' };
// ── Element references ──
const inputText = document.getElementById('input-text');
const charCount = document.getElementById('char-count');
const wordCount = document.getElementById('word-count');
const btnCorrect = document.getElementById('btn-correct');
const btnClear = document.getElementById('btn-clear');
const btnApplyAll = document.getElementById('btn-apply-all');
const btnApplyPage = document.getElementById('btn-apply-page');
const btnCopyText = document.getElementById('btn-copy-text');
const scoreSection = document.getElementById('score-section');
const suggestionsSection = document.getElementById('suggestions-section');
const suggestionsList = document.getElementById('suggestions-list');
const timingSection = document.getElementById('timing-section');
const timingText = document.getElementById('timing-text');
const loadingOverlay = document.getElementById('loading-overlay');
const loadingTextEl = document.getElementById('loading-text');
// Summary tab
const summaryInputText = document.getElementById('summary-input-text');
const summaryWordCountInput = document.getElementById('summary-word-count-input');
const btnSummarize = document.getElementById('btn-summarize');
const summaryResultSection = document.getElementById('summary-result-section');
const summaryText = document.getElementById('summary-text');
const summaryStats = document.getElementById('summary-stats');
const summaryWordCount = document.getElementById('summary-word-count');
const summaryCompression = document.getElementById('summary-compression');
const btnCopySummary = document.getElementById('btn-copy-summary');
// Score
const scoreValue = document.getElementById('score-value');
const scoreCircle = document.getElementById('score-circle');
const scoreHint = document.getElementById('score-hint');
const countSpelling = document.getElementById('count-spelling');
const countGrammar = document.getElementById('count-grammar');
const countPunctuation = document.getElementById('count-punctuation');
// ══════════════════════════════════════════════════════════
// State
// ══════════════════════════════════════════════════════════
let currentSuggestions = [];
let analyzedText = '';
let isStale = false;
let isAnalyzing = false;
let contextConsumed = false;
let sourceSelectionText = '';
let summaryMode = 'paragraph';
const _dismissedWords = new Set(
JSON.parse(localStorage.getItem('bayan_dismissed_words') || '[]')
);
function _saveDismissedWords() {
try { localStorage.setItem('bayan_dismissed_words', JSON.stringify([..._dismissedWords])); } catch {}
}
const SCORE_CIRCUMFERENCE = 440;
// ══════════════════════════════════════════════════════════
// Tab switching
// ══════════════════════════════════════════════════════════
document.querySelectorAll('.sp-tab').forEach((tab) => {
tab.addEventListener('click', () => {
const targetTab = tab.dataset.tab;
document.querySelectorAll('.sp-tab').forEach((t) => {
t.classList.toggle('active', t.dataset.tab === targetTab);
t.setAttribute('aria-selected', t.dataset.tab === targetTab ? 'true' : 'false');
});
document.querySelectorAll('.sp-panel').forEach((p) => {
p.classList.toggle('active', p.id === `panel-${targetTab}`);
});
saveState();
});
});
// ══════════════════════════════════════════════════════════
// Character & word counter
// ══════════════════════════════════════════════════════════
function updateCounts(textarea, charEl, wordEl) {
const text = textarea.value;
const chars = text.length;
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
if (charEl) charEl.textContent = chars.toLocaleString('ar-EG');
if (wordEl) wordEl.textContent = words.toLocaleString('ar-EG');
}
inputText.addEventListener('input', () => {
updateCounts(inputText, charCount, wordCount);
saveState();
if (currentSuggestions.length > 0 && inputText.value !== analyzedText) {
markStale();
}
});
summaryInputText.addEventListener('input', () => {
const text = summaryInputText.value.trim();
const words = text ? text.split(/\s+/).length : 0;
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
saveState();
});
// ══════════════════════════════════════════════════════════
// Staleness management
// ══════════════════════════════════════════════════════════
function markStale() {
if (isStale) return;
isStale = true;
if (suggestionsSection) suggestionsSection.classList.add('sp-stale');
showToast('⚠ النص تغيّر — أعد التحليل لتحديث الاقتراحات', 4000);
btnCorrect.innerHTML = `
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h5M20 20v-5h-5M4 9a8 8 0 0114-3M20 15a8 8 0 01-14 3"/></svg>
إعادة التحليل`;
}
function clearStale() {
isStale = false;
if (suggestionsSection) suggestionsSection.classList.remove('sp-stale');
btnCorrect.innerHTML = `
<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>
تحليل وتصحيح`;
}
// ══════════════════════════════════════════════════════════
// Loading & Toast
// ══════════════════════════════════════════════════════════
function setLoading(show, text = 'جارٍ التحليل...') {
loadingOverlay.classList.toggle('is-hidden', !show);
loadingTextEl.textContent = text;
}
function showToast(message, duration = 2500) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('is-visible');
clearTimeout(toast._timer);
toast._timer = setTimeout(() => toast.classList.remove('is-visible'), duration);
}
// ══════════════════════════════════════════════════════════
// Write-back to the page field (panel → background → content script)
// ══════════════════════════════════════════════════════════
function writeBackToPage(text, mode = 'auto', source = 'correct', find = '') {
try {
chrome.runtime.sendMessage(
{ type: 'WRITE_BACK_TO_PAGE', text, mode, source, find },
(resp) => {
if (resp && resp.ok) {
sourceSelectionText = text;
showToast('✓ تم تطبيق التغييرات في الصفحة');
} else showToast('تعذّر الكتابة في الصفحة — انسخ النص يدوياً');
}
);
} catch {
showToast('تعذّر الكتابة في الصفحة — انسخ النص يدوياً');
}
}
// ══════════════════════════════════════════════════════════
// Score ring
// ══════════════════════════════════════════════════════════
function updateScore(spelling, grammar, punctuation) {
const score = calculateWritingScore(spelling, grammar, punctuation);
const total = spelling + grammar + punctuation;
scoreSection.classList.remove('is-hidden');
if (scoreValue) {
const hasText = inputText.value.trim().length > 0;
scoreValue.textContent = (hasText || total > 0) ? score.toLocaleString('ar-EG') : '--';
}
if (scoreCircle) {
const offset = SCORE_CIRCUMFERENCE - (score / 100) * SCORE_CIRCUMFERENCE;
scoreCircle.style.strokeDashoffset = String(offset);
}
if (scoreHint) {
const hasText = inputText.value.trim().length > 0;
if (total === 0 && hasText) {
scoreHint.textContent = 'كتابة ممتازة! استمر.';
} else if (total === 0) {
scoreHint.innerHTML = 'ابدأ الكتابة لرؤية تقييمك<br><span class="sp-score-hint-sub">تحسين القواعد يرفع التقييم</span>';
} else {
scoreHint.textContent = getScoreHint(score, total);
}
}
if (countSpelling) countSpelling.textContent = spelling.toLocaleString('ar-EG');
if (countGrammar) countGrammar.textContent = grammar.toLocaleString('ar-EG');
if (countPunctuation) countPunctuation.textContent = punctuation.toLocaleString('ar-EG');
}
// ══════════════════════════════════════════════════════════
// Render suggestions list
// ══════════════════════════════════════════════════════════
function renderSuggestions(suggestions) {
currentSuggestions = suggestions;
if (!suggestions || suggestions.length === 0) {
btnApplyAll.classList.add('is-hidden');
if (analyzedText) {
suggestionsSection.classList.remove('is-hidden');
suggestionsList.innerHTML = '<div class="sp-empty-state"><svg class="sp-empty-state-icon" width="48" height="48" 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><p class="sp-empty-state-title">نصك ممتاز!</p><p class="sp-empty-state-desc">لم نجد أي أخطاء — أحسنت! ✨</p></div>';
} else {
suggestionsSection.classList.add('is-hidden');
}
return;
}
suggestionsSection.classList.remove('is-hidden');
suggestionsList.innerHTML = suggestions.map((s, i) => buildSuggestionCardHTML(s, i)).join('');
suggestionsList.querySelectorAll('.bayan-alt-chip').forEach((chip) => {
chip.addEventListener('click', (e) => {
e.stopPropagation();
if (isStale) {
showToast('⚠ أعد التحليل أولاً — النص تغيّر');
return;
}
const suggestionId = chip.dataset.cardId;
const altText = chip.dataset.cardAlt;
const suggestion = currentSuggestions.find((s) => String(s.id || '') === String(suggestionId));
if (!suggestion) return;
if (altText === suggestion.original) {
_dismissedWords.add(suggestion.original);
_saveDismissedWords();
currentSuggestions = removeSuggestion(currentSuggestions, suggestion.id);
} else {
const result = applyAndRebase(analyzedText, suggestion, altText, currentSuggestions);
analyzedText = result.text;
currentSuggestions = result.suggestions;
inputText.value = analyzedText;
updateCounts(inputText, charCount, wordCount);
}
const counts = countByType(currentSuggestions);
updateScore(counts.spelling, counts.grammar, counts.punctuation);
renderSuggestions(currentSuggestions);
saveState();
showToast('✓ تم التصحيح');
});
});
// Show apply-all only when more than 1 suggestion
if (suggestions.length > 1) {
btnApplyAll.textContent = 'تطبيق الكل (' + suggestions.length.toLocaleString('ar-EG') + ')';
btnApplyAll.classList.remove('is-hidden');
} else {
btnApplyAll.classList.add('is-hidden');
}
}
// ══════════════════════════════════════════════════════════
// Core analysis function
// ══════════════════════════════════════════════════════════
async function runAnalysis(text) {
if (!text || text.trim().split(/\s+/).length < 2) {
showToast('أدخل كلمتين على الأقل');
return;
}
if (text.length > CONFIG.MAX_ANALYZE_LENGTH) {
showToast('النص طويل جداً (الحد الأقصى ٥٠٠٠ حرف)');
return;
}
isAnalyzing = true;
setLoading(true, 'جارٍ التحليل...');
clearStale();
try {
const data = await bayanAnalyze(text);
if (data.status === 'success' || data.status === 'partial') {
const suggestions = sortSuggestions(data.suggestions || []).filter(
s => !_dismissedWords.has(s.original)
);
currentSuggestions = suggestions;
analyzedText = data.original;
inputText.value = analyzedText;
updateCounts(inputText, charCount, wordCount);
const counts = countByType(suggestions);
updateScore(counts.spelling, counts.grammar, counts.punctuation);
renderSuggestions(suggestions);
if (data.timing_ms) {
timingSection.classList.remove('is-hidden');
timingText.textContent = `التحليل: ${data.timing_ms.total_ms || 0}ms`;
}
saveState();
} else {
showToast('تعذّر التحليل — حاول مرة أخرى');
}
} catch (error) {
console.error('[Bayan SP] Analysis error:', error);
showToast('خطأ في الاتصال — تحقق من الإنترنت');
} finally {
isAnalyzing = false;
setLoading(false);
}
}
// ══════════════════════════════════════════════════════════
// State persistence (side panel survives navigation)
// ══════════════════════════════════════════════════════════
function getStorage() {
return chrome.storage?.session || chrome.storage?.local;
}
function saveState() {
const storage = getStorage();
if (!storage) return;
const activeTabEl = document.querySelector('.sp-tab.active');
const activeTab = activeTabEl ? activeTabEl.dataset.tab : 'correct';
storage.set({
spLastText: analyzedText,
spLastSuggestions: currentSuggestions,
spLastInput: inputText.value,
spLastSummarize: (document.getElementById('summary-input-text') || {}).value || '',
spLastDialect: (document.getElementById('dialect-input-text') || {}).value || '',
spLastQuran: (document.getElementById('quran-input-text') || {}).value || '',
spActiveTab: activeTab
}).catch(() => {});
}
async function restoreState() {
const storage = getStorage();
if (!storage) return false;
try {
const data = await storage.get(['spLastText', 'spLastSuggestions', 'spLastInput', 'spLastSummarize', 'spLastDialect', 'spLastQuran', 'spActiveTab']);
if (data.spLastSummarize) {
const sInput = document.getElementById('summary-input-text');
if (sInput) {
sInput.value = data.spLastSummarize;
const words = data.spLastSummarize.trim() ? data.spLastSummarize.trim().split(/\s+/).length : 0;
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
}
}
if (data.spLastDialect) {
const dInput = document.getElementById('dialect-input-text');
if (dInput) { dInput.value = data.spLastDialect; updateCounts(dInput, document.getElementById('dialect-char-count'), null); }
}
if (data.spLastQuran) {
const qInput = document.getElementById('quran-input-text');
if (qInput) { qInput.value = data.spLastQuran; updateCounts(qInput, document.getElementById('quran-char-count'), null); }
}
if (data.spActiveTab) {
const tabBtn = document.querySelector(`.sp-tab[data-tab="${data.spActiveTab}"]`);
if (tabBtn) tabBtn.click();
}
if (!data.spLastText || !data.spLastSuggestions) return false;
analyzedText = data.spLastText;
currentSuggestions = data.spLastSuggestions;
inputText.value = data.spLastInput || analyzedText;
updateCounts(inputText, charCount, wordCount);
const counts = countByType(currentSuggestions);
updateScore(counts.spelling, counts.grammar, counts.punctuation);
renderSuggestions(currentSuggestions);
return true;
} catch {
return false;
}
}
// ══════════════════════════════════════════════════════════
// Button handlers
// ══════════════════════════════════════════════════════════
btnCorrect.addEventListener('click', () => {
const text = inputText.value.trim();
runAnalysis(text);
});
btnClear.addEventListener('click', () => {
inputText.value = '';
analyzedText = '';
sourceSelectionText = '';
updateCounts(inputText, charCount, wordCount);
scoreSection.classList.add('is-hidden');
suggestionsSection.classList.add('is-hidden');
timingSection.classList.add('is-hidden');
currentSuggestions = [];
clearStale();
const storage = getStorage();
if (storage) storage.remove(['spLastText', 'spLastSuggestions', 'spLastInput']).catch(() => {});
});
btnApplyAll.addEventListener('click', () => {
if (currentSuggestions.length === 0) return;
if (isStale) {
showToast('⚠ أعد التحليل أولاً — النص تغيّر');
return;
}
analyzedText = applyAllPatches(analyzedText, currentSuggestions);
inputText.value = analyzedText;
updateCounts(inputText, charCount, wordCount);
currentSuggestions = [];
updateScore(0, 0, 0);
renderSuggestions([]);
saveState();
writeBackToPage(analyzedText, 'auto', 'correct', sourceSelectionText);
showToast('✓ تم تطبيق جميع التصحيحات');
});
if (btnApplyPage) {
btnApplyPage.addEventListener('click', () => {
if (!analyzedText && !inputText.value.trim()) { showToast('لا يوجد نص للتطبيق'); return; }
if (isStale) { showToast('⚠ أعد التحليل أولاً — النص تغيّر'); return; }
const finalText = currentSuggestions.length > 0
? applyAllPatches(analyzedText, currentSuggestions)
: (analyzedText || inputText.value.trim());
writeBackToPage(finalText, 'auto', 'correct', sourceSelectionText);
});
}
if (btnCopyText) {
btnCopyText.addEventListener('click', () => {
const text = inputText.value || '';
navigator.clipboard.writeText(text)
.then(() => showToast('✓ تم نسخ النص'))
.catch(() => showToast('تعذّر النسخ'));
});
}
// ══════════════════════════════════════════════════════════
// Summary mode toggle (paragraph / bullets)
// ══════════════════════════════════════════════════════════
document.querySelectorAll('.sp-mode-btn').forEach((btn) => {
btn.addEventListener('click', () => {
summaryMode = btn.dataset.mode;
document.querySelectorAll('.sp-mode-btn').forEach((b) => {
b.classList.toggle('active', b.dataset.mode === summaryMode);
});
});
});
// ══════════════════════════════════════════════════════════
// Summarize
// ══════════════════════════════════════════════════════════
btnSummarize.addEventListener('click', async () => {
const text = summaryInputText.value.trim();
if (!text) { showToast('أدخل نصاً للتلخيص'); return; }
if (text.length < CONFIG.MIN_SUMMARIZE_LENGTH) { showToast('النص قصير جداً للتلخيص'); return; }
const lengthValue = parseInt(document.querySelector('input[name="summary-length"]:checked')?.value || '2', 10);
setLoading(true, 'جارٍ التلخيص...');
try {
const data = await bayanSummarize(text, lengthValue);
if (data.status === 'success' && data.summary) {
summaryResultSection.classList.remove('is-hidden');
const summaryContent = data.summary;
if (summaryMode === 'bullets') {
const sentences = summaryContent.split(/[.،؛]\s*/).filter(s => s.trim().length > 2);
const ul = document.createElement('ul');
ul.style.cssText = 'list-style: disc; padding-right: 1.5rem; direction: rtl; text-align: right;';
sentences.forEach(s => {
const li = document.createElement('li');
li.textContent = s.trim();
li.style.marginBottom = '8px';
ul.appendChild(li);
});
summaryText.textContent = '';
summaryText.appendChild(ul);
} else {
summaryText.textContent = summaryContent;
}
const origWords = text.trim().split(/\s+/).length;
const sumWords = summaryContent.trim().split(/\s+/).length;
const compression = origWords > 0 ? Math.round((1 - sumWords / origWords) * 100) : 0;
if (summaryStats) {
summaryStats.classList.remove('is-hidden');
if (summaryWordCount) summaryWordCount.textContent = sumWords.toLocaleString('ar-EG');
if (summaryCompression) summaryCompression.textContent = compression.toLocaleString('ar-EG') + '٪';
}
showToast('✓ تم التلخيص');
} else {
showToast('تعذّر التلخيص — حاول مرة أخرى');
}
} catch (error) {
console.error('[Bayan SP] Summarization error:', error);
showToast('خطأ في الاتصال — تحقق من الإنترنت');
} finally {
setLoading(false);
}
});
btnCopySummary.addEventListener('click', () => {
const text = summaryText.innerText || summaryText.textContent || '';
navigator.clipboard.writeText(text)
.then(() => showToast('✓ تم نسخ الملخص'))
.catch(() => showToast('تعذّر النسخ'));
});
const btnApplySummary = document.getElementById('btn-apply-summary');
if (btnApplySummary) {
btnApplySummary.addEventListener('click', () => {
const text = summaryText.innerText || summaryText.textContent || '';
if (!text) { showToast('لا يوجد ملخص للتطبيق'); return; }
writeBackToPage(text, 'auto', 'summarize', sourceSelectionText);
});
}
// ══════════════════════════════════════════════════════════
// Summary: File import (.txt / .docx)
// ══════════════════════════════════════════════════════════
const summaryImportInput = document.getElementById('summary-import-input');
if (summaryImportInput) {
summaryImportInput.addEventListener('change', (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;
if (file.name.endsWith('.txt')) {
const reader = new FileReader();
reader.onload = (ev) => {
summaryInputText.value = ev.target.result;
const words = summaryInputText.value.trim() ? summaryInputText.value.trim().split(/\s+/).length : 0;
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
showToast('✓ تم استيراد الملف');
saveState();
};
reader.readAsText(file, 'UTF-8');
} else if (file.name.endsWith('.docx')) {
if (typeof mammoth !== 'undefined') {
const reader = new FileReader();
reader.onload = (ev) => {
mammoth.extractRawText({ arrayBuffer: ev.target.result })
.then((result) => {
summaryInputText.value = result.value;
const words = result.value.trim() ? result.value.trim().split(/\s+/).length : 0;
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
showToast('✓ تم استيراد الملف');
saveState();
})
.catch(() => showToast('خطأ في قراءة الملف'));
};
reader.readAsArrayBuffer(file);
} else {
showToast('صيغة .docx غير مدعومة — استخدم .txt');
}
}
e.target.value = '';
});
}
// ══════════════════════════════════════════════════════════
// Summary: Export dropdown (.txt / .docx / .pdf)
// ══════════════════════════════════════════════════════════
const btnExportSummary = document.getElementById('btn-export-summary');
const summaryExportMenu = document.getElementById('summary-export-menu');
if (btnExportSummary && summaryExportMenu) {
btnExportSummary.addEventListener('click', (e) => {
e.stopPropagation();
summaryExportMenu.classList.toggle('is-hidden');
});
document.addEventListener('click', () => {
summaryExportMenu.classList.add('is-hidden');
});
summaryExportMenu.addEventListener('click', (e) => {
e.stopPropagation();
});
summaryExportMenu.querySelectorAll('.sp-export-item').forEach((item) => {
item.addEventListener('click', () => {
const format = item.dataset.format;
const text = (summaryText.innerText || summaryText.textContent || '').trim();
if (!text) { showToast('لا يوجد ملخص للتصدير'); return; }
summaryExportMenu.classList.add('is-hidden');
exportSummary(format, text);
});
});
}
function exportSummary(format, text) {
if (format === 'txt') {
downloadFile(text, 'ملخص-بيان.txt', 'text/plain;charset=utf-8');
showToast('✓ تم تصدير الملخص');
} else if (format === 'docx') {
if (typeof docx === 'undefined') { showToast('مكتبة Word غير محمّلة'); return; }
try {
const paragraphs = text.split(/\n+/).filter(p => p.trim());
const children = paragraphs.map(block =>
new docx.Paragraph({
bidirectional: true,
alignment: docx.AlignmentType.RIGHT,
children: [new docx.TextRun({ text: block, rightToLeft: true, font: 'Arial' })]
})
);
const doc = new docx.Document({ sections: [{ properties: { rightToLeft: true }, children }] });
docx.Packer.toBlob(doc).then((blob) => {
downloadBlob(blob, 'ملخص-بيان.docx');
showToast('✓ تم تصدير الملخص كـ Word');
}).catch(() => showToast('تعذر تصدير ملف Word'));
} catch { showToast('تعذر تصدير ملف Word'); }
} else if (format === 'pdf') {
if (typeof html2pdf === 'undefined') { showToast('مكتبة PDF غير محمّلة'); return; }
showToast('جاري تصدير PDF...');
const html = '<div dir="rtl" style="font-family:Arial,sans-serif;font-size:16px;line-height:2;text-align:right;padding:20px;">' +
text.split(/\n+/).map(p => '<p>' + p + '</p>').join('') + '</div>';
html2pdf().set({
margin: [15, 15, 15, 15],
filename: 'ملخص-بيان.pdf',
image: { type: 'jpeg', quality: 0.95 },
html2canvas: { scale: 1.5, useCORS: true, backgroundColor: '#ffffff' },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}).from(html, 'string').save()
.then(() => showToast('✓ تم تصدير الملخص كـ PDF'))
.catch(() => showToast('تعذر تصدير PDF'));
}
}
function downloadFile(text, filename, mime) {
const blob = new Blob([text], { type: mime });
downloadBlob(blob, filename);
}
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
// ══════════════════════════════════════════════════════════
// Dialect → MSA conversion
// ══════════════════════════════════════════════════════════
const dialectInput = document.getElementById('dialect-input-text');
const dialectCharCount = document.getElementById('dialect-char-count');
const btnDialect = document.getElementById('btn-dialect');
const dialectResultSection = document.getElementById('dialect-result-section');
const dialectText = document.getElementById('dialect-text');
const btnCopyDialect = document.getElementById('btn-copy-dialect');
if (dialectInput) {
dialectInput.addEventListener('input', () => {
const chars = dialectInput.value.length;
if (dialectCharCount) dialectCharCount.textContent = chars.toLocaleString('ar-EG');
saveState();
});
btnDialect.addEventListener('click', async () => {
const text = dialectInput.value.trim();
if (!text) { showToast('أدخل نصاً للتحويل'); return; }
setLoading(true, 'جارٍ التحويل...');
try {
const data = await bayanDialect(text);
if (data.status === 'success' && data.converted_text) {
dialectResultSection.classList.remove('is-hidden');
dialectText.textContent = data.converted_text;
showToast('✓ تم التحويل');
} else {
showToast(data.error || 'تعذّر التحويل — حاول مرة أخرى');
}
} catch (error) {
console.error('[Bayan SP] Dialect error:', error);
showToast('خطأ في الاتصال — تحقق من الإنترنت');
} finally {
setLoading(false);
}
});
btnCopyDialect.addEventListener('click', () => {
navigator.clipboard.writeText(dialectText.textContent || '')
.then(() => showToast('✓ تم نسخ النص'))
.catch(() => showToast('تعذّر النسخ'));
});
const btnApplyDialect = document.getElementById('btn-apply-dialect');
if (btnApplyDialect) {
btnApplyDialect.addEventListener('click', () => {
const text = dialectText.textContent || '';
if (!text) { showToast('لا يوجد نص للتطبيق'); return; }
writeBackToPage(text, 'auto', 'dialect', sourceSelectionText);
});
}
}
// ══════════════════════════════════════════════════════════
// Quran verification + translation (multi-match)
// ══════════════════════════════════════════════════════════
const quranInput = document.getElementById('quran-input-text');
const quranCharCount = document.getElementById('quran-char-count');
const btnQuran = document.getElementById('btn-quran');
const quranResultSection = document.getElementById('quran-result-section');
const quranMatchList = document.getElementById('quran-match-list');
let _quranQuery = '';
function _parseSegment(seg) {
const refMatch = seg.match(/【([^】]+)】/);
const verseText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
const reference = refMatch ? refMatch[1] : '';
return { verseText, reference };
}
function _langOptionsHTML() {
return '<option value="">— اختر لغة —</option>'
+ '<option value="english">English</option>'
+ '<option value="french">Français</option>'
+ '<option value="turkish">Türkçe</option>'
+ '<option value="persian">فارسی</option>'
+ '<option value="russian">Русский</option>'
+ '<option value="spanish">Español</option>'
+ '<option value="german">Deutsch</option>'
+ '<option value="indonesian">Indonesia</option>'
+ '<option value="malay">Melayu</option>'
+ '<option value="bengali">বাংলা</option>'
+ '<option value="bosnian">Bosanski</option>'
+ '<option value="portuguese">Português</option>'
+ '<option value="uzbek">O&#x2019;zbek</option>';
}
function _renderMatchCards(matches) {
quranMatchList.innerHTML = '';
matches.forEach(function(m, i) {
var parsed = _parseSegment(m.matched_segment || '');
var card = document.createElement('div');
card.className = 'sp-quran-match-card';
card.innerHTML = '<div class="sp-quran-result-header">'
+ '<span style="color:#06b6d4;font-size:12px;font-weight:700;">✓ نتيجة ' + (i + 1) + '</span>'
+ '<div class="sp-result-actions">'
+ '<button class="sp-btn-icon qmc-apply" type="button" title="تطبيق في الصفحة">'
+ '<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>'
+ '</button>'
+ '<button class="sp-btn-icon qmc-copy" type="button" title="نسخ">'
+ '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>'
+ '</button></div></div>'
+ '<p class="sp-quran-uthmani" dir="rtl">' + _escHTML(parsed.verseText) + '</p>'
+ '<p class="sp-quran-reference">' + (parsed.reference ? '[' + _escHTML(parsed.reference) + ']' : '') + '</p>'
+ '<div class="sp-quran-translate">'
+ '<div class="sp-quran-translate-row">'
+ '<span style="font-size:12px;font-weight:600;color:var(--text-secondary);">ترجمة الآية</span>'
+ '<select class="sp-select qmc-lang">' + _langOptionsHTML() + '</select>'
+ '</div>'
+ '<div class="sp-quran-translation is-hidden qmc-trans-section">'
+ '<p class="qmc-trans-text" dir="auto"></p>'
+ '<div class="sp-result-actions" style="justify-content:flex-end;gap:4px;margin-top:4px;">'
+ '<button class="sp-btn-icon qmc-apply-trans" type="button" title="تطبيق الترجمة">'
+ '<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>'
+ '</button>'
+ '<button class="sp-btn-icon qmc-copy-trans" type="button" title="نسخ الترجمة">'
+ '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>'
+ '</button></div></div></div>';
card.querySelector('.qmc-copy').addEventListener('click', function() {
var txt = parsed.verseText + (parsed.reference ? ' [' + parsed.reference + ']' : '');
navigator.clipboard.writeText(txt)
.then(function() { showToast('✓ تم النسخ'); })
.catch(function() { showToast('تعذّر النسخ'); });
});
card.querySelector('.qmc-apply').addEventListener('click', function() {
var txt = parsed.verseText + (parsed.reference ? ' [' + parsed.reference + ']' : '');
writeBackToPage(txt, 'auto', 'quran', sourceSelectionText);
});
var langSel = card.querySelector('.qmc-lang');
var transSec = card.querySelector('.qmc-trans-section');
var transP = card.querySelector('.qmc-trans-text');
var copyTransBtn = card.querySelector('.qmc-copy-trans');
var applyTransBtn = card.querySelector('.qmc-apply-trans');
langSel.addEventListener('change', async function() {
var lang = langSel.value;
if (!lang || !_quranQuery) return;
transSec.classList.remove('is-hidden');
transP.textContent = '⏳ جاري الترجمة...';
try {
var data = await bayanQuran(_quranQuery, lang);
if (data.error) { transP.textContent = data.error; return; }
var seg = data.matched_segment || '';
var p = _parseSegment(seg);
transP.textContent = p.verseText;
} catch (err) {
transP.textContent = 'حدث خطأ في الترجمة';
}
});
copyTransBtn.addEventListener('click', function() {
navigator.clipboard.writeText(transP.textContent || '')
.then(function() { showToast('✓ تم نسخ الترجمة'); })
.catch(function() { showToast('تعذّر النسخ'); });
});
applyTransBtn.addEventListener('click', function() {
var txt = transP.textContent || '';
if (!txt) { showToast('لا يوجد ترجمة للتطبيق'); return; }
writeBackToPage(txt, 'auto', 'quran', sourceSelectionText);
});
quranMatchList.appendChild(card);
});
}
function _escHTML(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
if (quranInput) {
quranInput.addEventListener('input', () => { updateCounts(quranInput, quranCharCount, null); saveState(); });
btnQuran.addEventListener('click', async () => {
const text = quranInput.value.trim();
if (!text) { showToast('أدخل آية للتدقيق'); return; }
_quranQuery = text;
setLoading(true, 'جارٍ التدقيق...');
try {
const data = await bayanQuran(text, 'تدقيق الايات', null, 5);
quranResultSection.classList.remove('is-hidden');
if (data.error) {
quranMatchList.innerHTML = '<p style="color:var(--text-secondary);font-size:13px;padding:8px;">' + _escHTML(data.error) + '</p>';
return;
}
var matches = data.matches || [data];
_renderMatchCards(matches);
showToast('✓ تم التدقيق');
} catch (error) {
console.error('[Bayan SP] Quran error:', error);
quranResultSection.classList.remove('is-hidden');
quranMatchList.innerHTML = '<p style="color:var(--text-secondary);font-size:13px;padding:8px;">خطأ في الاتصال — تحقق من الإنترنت</p>';
} finally {
setLoading(false);
}
});
}
// ══════════════════════════════════════════════════════════
// Status check
// ══════════════════════════════════════════════════════════
(async function checkStatus() {
const statusDot = document.querySelector('.sp-status-dot');
const statusText = document.getElementById('status-text');
try {
await bayanHealthCheck();
if (statusDot) statusDot.classList.add('online');
if (statusText) statusText.textContent = 'متصل';
} catch {
if (statusDot) statusDot.classList.add('offline');
if (statusText) statusText.textContent = 'غير متصل';
}
})();
// ══════════════════════════════════════════════════════════
// Context menu pickup
// ══════════════════════════════════════════════════════════
function runContextAction(action, text) {
sourceSelectionText = text;
if (action === TAB.CORRECT) {
inputText.value = text;
updateCounts(inputText, charCount, wordCount);
document.querySelector(`[data-tab="${TAB.CORRECT}"]`)?.click();
setTimeout(() => runAnalysis(text), 100);
} else if (action === TAB.SUMMARIZE) {
summaryInputText.value = text;
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
document.querySelector(`[data-tab="${TAB.SUMMARIZE}"]`)?.click();
setTimeout(() => btnSummarize.click(), 100);
} else if (action === TAB.DIALECT && dialectInput && btnDialect) {
dialectInput.value = text;
const chars = text.length;
if (dialectCharCount) dialectCharCount.textContent = chars.toLocaleString('ar-EG');
document.querySelector(`[data-tab="${TAB.DIALECT}"]`)?.click();
setTimeout(() => btnDialect.click(), 120);
} else if (action === TAB.QURAN && quranInput && btnQuran) {
quranInput.value = text;
updateCounts(quranInput, quranCharCount, null);
document.querySelector(`[data-tab="${TAB.QURAN}"]`)?.click();
setTimeout(() => btnQuran.click(), 120);
}
}
async function tryPickupContext(retryCount = 0) {
if (typeof chrome === 'undefined' || !chrome.storage) return;
if (contextConsumed) return;
const storage = getStorage();
if (!storage) return;
try {
const data = await storage.get(['contextAction', 'contextText', 'contextTimestamp']);
if ((!data.contextAction || !data.contextText) && retryCount < 2) {
setTimeout(() => tryPickupContext(retryCount + 1), 300);
return;
}
if (!data.contextAction || !data.contextText) {
await restoreState();
return;
}
const age = Date.now() - (data.contextTimestamp || 0);
if (age > 15000) {
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
await restoreState();
return;
}
contextConsumed = true;
console.log(`[Bayan SP] Context action: ${data.contextAction}, text: ${data.contextText.length} chars`);
runContextAction(data.contextAction, data.contextText);
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
} catch (err) {
console.warn('[Bayan SP] Context action check failed:', err);
await restoreState();
}
}
tryPickupContext(0);
// ══════════════════════════════════════════════════════════
// Listen for NEW context actions while panel is already open
// ══════════════════════════════════════════════════════════
if (chrome.storage?.session?.onChanged || chrome.storage?.onChanged) {
const storageApi = chrome.storage;
storageApi.onChanged.addListener((changes, area) => {
if (area !== 'session' && area !== 'local') return;
if (!changes.contextAction || !changes.contextText) return;
const action = changes.contextAction.newValue;
const text = changes.contextText.newValue;
if (!action || !text) return;
console.log(`[Bayan SP] Storage changed — new context: ${action}, ${text.length} chars`);
runContextAction(action, text);
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
});
}
});
// ── Theme Toggle Logic ──
(function initBayanThemeToggle() {
const toggleBtn = document.getElementById('ext-theme-toggle');
chrome.storage.local.get(['theme'], (result) => {
const currentTheme = result.theme || 'dark';
document.documentElement.setAttribute('data-theme', currentTheme);
});
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === 'local' && changes.theme) {
document.documentElement.setAttribute('data-theme', changes.theme.newValue);
}
});
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
let theme = document.documentElement.getAttribute('data-theme') || 'dark';
let targetTheme = theme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', targetTheme);
chrome.storage.local.set({ theme: targetTheme });
});
}
})();