| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| document.addEventListener('DOMContentLoaded', () => { |
| |
| const TAB = { CORRECT: 'correct', SUMMARIZE: 'summarize', DIALECT: 'dialect', QURAN: 'quran' }; |
|
|
| |
| 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'); |
|
|
| |
| 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'); |
|
|
| |
| 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'); |
|
|
| |
| |
| |
| 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; |
|
|
| |
| |
| |
| 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(); |
| }); |
| }); |
|
|
| |
| |
| |
| 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(); |
| }); |
|
|
| |
| |
| |
| 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> |
| تحليل وتصحيح`; |
| } |
|
|
| |
| |
| |
| 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); |
| } |
|
|
| |
| |
| |
| 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('تعذّر الكتابة في الصفحة — انسخ النص يدوياً'); |
| } |
| } |
|
|
| |
| |
| |
| 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'); |
| } |
|
|
| |
| |
| |
| 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('✓ تم التصحيح'); |
| }); |
| }); |
|
|
| |
| if (suggestions.length > 1) { |
| btnApplyAll.textContent = 'تطبيق الكل (' + suggestions.length.toLocaleString('ar-EG') + ')'; |
| btnApplyAll.classList.remove('is-hidden'); |
| } else { |
| btnApplyAll.classList.add('is-hidden'); |
| } |
| } |
|
|
| |
| |
| |
| 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); |
| } |
| } |
|
|
| |
| |
| |
| 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; |
| } |
| } |
|
|
| |
| |
| |
| 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('تعذّر النسخ')); |
| }); |
| } |
|
|
| |
| |
| |
| 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); |
| }); |
| }); |
| }); |
|
|
| |
| |
| |
| 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); |
| }); |
| } |
|
|
| |
| |
| |
| 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 = ''; |
| }); |
| } |
|
|
| |
| |
| |
| 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); |
| } |
|
|
| |
| |
| |
| 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); |
| }); |
| } |
| } |
|
|
| |
| |
| |
| 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’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); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| (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 = 'غير متصل'; |
| } |
| })(); |
|
|
| |
| |
| |
| 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); |
|
|
| |
| |
| |
| 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' }); |
| }); |
| } |
| }); |
|
|
|
|
| |
| (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 }); |
| }); |
| } |
| })(); |
|
|