| |
| |
| |
| |
| |
| |
| |
| |
|
|
| document.addEventListener('DOMContentLoaded', () => { |
| |
| 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 btnCopyResult = document.getElementById('btn-copy-result'); |
| const scoreSection = document.getElementById('score-section'); |
| const resultSection = document.getElementById('result-section'); |
| const resultText = document.getElementById('result-text'); |
| 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 summaryCharCount = document.getElementById('summary-char-count'); |
| const btnSummarize = document.getElementById('btn-summarize'); |
| const summaryResultSection = document.getElementById('summary-result-section'); |
| const summaryText = document.getElementById('summary-text'); |
| const summaryMeta = document.getElementById('summary-meta'); |
| 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; |
|
|
| const SCORE_CIRCUMFERENCE = 440; |
|
|
| |
| |
| |
| document.querySelectorAll('.bayan-tab').forEach((tab) => { |
| tab.addEventListener('click', () => { |
| const targetTab = tab.dataset.tab; |
| document.querySelectorAll('.bayan-tab').forEach((t) => { |
| t.classList.toggle('active', t.dataset.tab === targetTab); |
| t.setAttribute('aria-selected', t.dataset.tab === targetTab ? 'true' : 'false'); |
| }); |
| document.querySelectorAll('.bayan-panel').forEach((p) => { |
| p.classList.toggle('active', p.id === `panel-${targetTab}`); |
| }); |
| }); |
| }); |
|
|
| |
| |
| |
| 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); |
|
|
| |
| if (currentSuggestions.length > 0 && inputText.value !== analyzedText) { |
| markStale(); |
| } |
| }); |
|
|
| summaryInputText.addEventListener('input', () => updateCounts(summaryInputText, summaryCharCount, null)); |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| function markStale() { |
| if (isStale) return; |
| isStale = true; |
|
|
| |
| if (resultSection) resultSection.classList.add('bayan-stale'); |
| if (suggestionsSection) suggestionsSection.classList.add('bayan-stale'); |
|
|
| |
| showToast('⚠ النص تغيّر — أعد التحليل لتحديث الاقتراحات', 4000); |
|
|
| |
| btnCorrect.innerHTML = ` |
| <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h5M20 20v-5h-5M4 9a8 8 0 0114-3M20 15a8 8 0 01-14 3"/></svg> |
| إعادة التحليل`; |
| } |
|
|
| |
| |
| |
| function clearStale() { |
| isStale = false; |
| if (resultSection) resultSection.classList.remove('bayan-stale'); |
| if (suggestionsSection) suggestionsSection.classList.remove('bayan-stale'); |
|
|
| |
| btnCorrect.innerHTML = ` |
| <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4"/></svg> |
| تحليل وتصحيح`; |
| } |
|
|
| |
| |
| |
| 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 updateScore(spelling, grammar, punctuation) { |
| const score = calculateWritingScore(spelling, grammar, punctuation); |
| const total = spelling + grammar + punctuation; |
|
|
| scoreSection.classList.remove('is-hidden'); |
|
|
| if (scoreValue) { |
| scoreValue.textContent = score > 0 || total > 0 ? score.toLocaleString('ar-EG') : '--'; |
| } |
| if (scoreCircle) { |
| const offset = SCORE_CIRCUMFERENCE - (score / 100) * SCORE_CIRCUMFERENCE; |
| scoreCircle.style.strokeDashoffset = String(offset); |
| } |
| if (scoreHint) { |
| 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) { |
| suggestionsSection.classList.add('is-hidden'); |
| return; |
| } |
|
|
| suggestionsSection.classList.remove('is-hidden'); |
| const html = suggestions.map((s, i) => buildSuggestionCardHTML(s, i)).join(''); |
| suggestionsList.innerHTML = html; |
|
|
| |
| 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) { |
| |
| 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); |
|
|
| |
| resultText.innerHTML = renderHighlightedText(analyzedText, currentSuggestions); |
|
|
| showToast('✓ تم التصحيح'); |
| }); |
| }); |
|
|
| |
| btnApplyAll.classList.toggle('is-hidden', suggestions.length < 2); |
| } |
|
|
| |
| |
| |
| btnClear.addEventListener('click', () => { |
| inputText.value = ''; |
| analyzedText = ''; |
| updateCounts(inputText, charCount, wordCount); |
| scoreSection.classList.add('is-hidden'); |
| resultSection.classList.add('is-hidden'); |
| suggestionsSection.classList.add('is-hidden'); |
| timingSection.classList.add('is-hidden'); |
| currentSuggestions = []; |
| clearStale(); |
| }); |
|
|
| |
| |
| |
| btnCorrect.addEventListener('click', async () => { |
| const text = inputText.value.trim(); |
| if (!text) { |
| showToast('أدخل نصاً للتحليل'); |
| return; |
| } |
| if (text.length < CONFIG.MIN_ANALYZE_LENGTH) { |
| showToast('النص قصير جداً (الحد الأدنى ١٥ حرفاً)'); |
| return; |
| } |
| if (text.length > CONFIG.MAX_ANALYZE_LENGTH) { |
| showToast('النص طويل جداً (الحد الأقصى ٥٠٠٠ حرف)'); |
| return; |
| } |
|
|
| setLoading(true, 'جارٍ التحليل...'); |
| clearStale(); |
|
|
| try { |
| const data = await bayanAnalyze(text); |
|
|
| if (data.status === 'success' || data.status === 'partial') { |
| const suggestions = sortSuggestions(data.suggestions || []); |
| currentSuggestions = suggestions; |
|
|
| |
| analyzedText = data.original; |
| |
| inputText.value = analyzedText; |
| updateCounts(inputText, charCount, wordCount); |
|
|
| |
| resultSection.classList.remove('is-hidden'); |
| resultText.innerHTML = renderHighlightedText(analyzedText, suggestions); |
|
|
| |
| 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 (إملائي: ${data.timing_ms.spelling_ms || 0}ms، نحوي: ${data.timing_ms.grammar_ms || 0}ms، ترقيم: ${data.timing_ms.punctuation_ms || 0}ms)`; |
| } |
|
|
| if (suggestions.length === 0) { |
| showToast('نصك ممتاز! لم نجد أي أخطاء ✨'); |
| } |
| } else { |
| showToast('تعذّر التحليل — حاول مرة أخرى'); |
| } |
| } catch (error) { |
| console.error('[Bayan] Analysis error:', error); |
| showToast('خطأ في الاتصال — تحقق من الإنترنت'); |
| } finally { |
| setLoading(false); |
| } |
| }); |
|
|
| |
| |
| |
| btnApplyAll.addEventListener('click', () => { |
| if (currentSuggestions.length === 0) return; |
|
|
| |
| if (isStale) { |
| showToast('⚠ أعد التحليل أولاً — النص تغيّر'); |
| return; |
| } |
|
|
| |
| analyzedText = applyAllPatches(analyzedText, currentSuggestions); |
| inputText.value = analyzedText; |
| updateCounts(inputText, charCount, wordCount); |
| currentSuggestions = []; |
| resultText.innerHTML = escapeHtml(analyzedText); |
| updateScore(0, 0, 0); |
| renderSuggestions([]); |
| showToast('✓ تم تطبيق جميع التصحيحات'); |
| }); |
|
|
| |
| |
| |
| btnCopyResult.addEventListener('click', () => { |
| const text = resultText.textContent || ''; |
| navigator.clipboard.writeText(text) |
| .then(() => showToast('✓ تم نسخ النص')) |
| .catch(() => showToast('تعذّر النسخ')); |
| }); |
|
|
| |
| |
| |
| 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'); |
| summaryText.textContent = data.summary; |
| summaryMeta.textContent = `النص الأصلي: ${(data.original_length || 0).toLocaleString('ar-EG')} حرف → الملخص: ${(data.summary_length || 0).toLocaleString('ar-EG')} حرف`; |
| showToast('✓ تم التلخيص'); |
| } else { |
| showToast('تعذّر التلخيص — حاول مرة أخرى'); |
| } |
| } catch (error) { |
| console.error('[Bayan] Summarization error:', error); |
| showToast('خطأ في الاتصال — تحقق من الإنترنت'); |
| } finally { |
| setLoading(false); |
| } |
| }); |
|
|
| |
| |
| |
| btnCopySummary.addEventListener('click', () => { |
| const text = summaryText.textContent || ''; |
| navigator.clipboard.writeText(text) |
| .then(() => showToast('✓ تم نسخ الملخص')) |
| .catch(() => showToast('تعذّر النسخ')); |
| }); |
|
|
| |
| |
| |
| (async function checkStatus() { |
| const statusDot = document.querySelector('.bayan-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 = 'غير متصل'; |
| } |
| })(); |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| const TAB_ACTIONS = { correct: 'correct', summarize: 'summarize' }; |
| let contextConsumed = false; |
|
|
| (async function checkContextAction() { |
| |
| if (typeof chrome === 'undefined' || !chrome.storage) return; |
|
|
| |
| if (contextConsumed) return; |
|
|
| |
| const storage = chrome.storage?.session || chrome.storage?.local; |
| if (!storage) return; |
|
|
| try { |
| const data = await storage.get(['contextAction', 'contextText', 'contextTimestamp']); |
| if (!data.contextAction || !data.contextText) return; |
|
|
| |
| const age = Date.now() - (data.contextTimestamp || 0); |
| if (age > 15000) { |
| chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' }); |
| return; |
| } |
|
|
| |
| contextConsumed = true; |
|
|
| console.log(`[Bayan] Context action: ${data.contextAction}, text length: ${data.contextText.length}`); |
|
|
| if (data.contextAction === TAB_ACTIONS.correct) { |
| |
| inputText.value = data.contextText; |
| updateCounts(inputText, charCount, wordCount); |
|
|
| |
| const correctTab = document.querySelector(`[data-tab="${TAB_ACTIONS.correct}"]`); |
| if (correctTab) correctTab.click(); |
|
|
| |
| setTimeout(() => btnCorrect.click(), 150); |
|
|
| } else if (data.contextAction === TAB_ACTIONS.summarize) { |
| |
| summaryInputText.value = data.contextText; |
| updateCounts(summaryInputText, summaryCharCount, null); |
|
|
| |
| const summarizeTab = document.querySelector(`[data-tab="${TAB_ACTIONS.summarize}"]`); |
| if (summarizeTab) summarizeTab.click(); |
|
|
| |
| setTimeout(() => btnSummarize.click(), 150); |
| } |
|
|
| |
| chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' }); |
|
|
| } catch (err) { |
| console.warn('[Bayan] Context action check failed:', err); |
| } |
| })(); |
| }); |
|
|
|
|