File size: 45,113 Bytes
74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 9c49fff 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 183ba6e 74411f9 183ba6e 74411f9 183ba6e 9c49fff 183ba6e 74411f9 9c49fff 74411f9 be32617 74411f9 9c49fff 74411f9 be32617 74411f9 be32617 74411f9 9c49fff 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 75e0099 be32617 75e0099 a10a9fe 75e0099 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 75e0099 74411f9 be32617 74411f9 75e0099 be32617 74411f9 be32617 75e0099 be32617 74411f9 be32617 75e0099 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 be32617 74411f9 42357d1 be32617 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 | /**
* Bayan Chrome Extension — Popup Logic
*
* Fixes applied:
* HIGH-1: Offset rebasing via applyAndRebase() after each individual apply
* MED-1: Clipboard .catch() on all writeText() calls
* MED-2: Staleness detection — locks corrections when user edits after analysis
*/
document.addEventListener('DOMContentLoaded', () => {
// ── 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 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 elements
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 elements
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 = [];
/**
* MED-2: Snapshot of the text that was last analyzed.
* All suggestion offsets are relative to THIS text.
* applyAndRebase() mutates this alongside suggestions.
*/
let analyzedText = '';
const _dismissedWords = new Set(
JSON.parse(localStorage.getItem('bayan_dismissed_words') || '[]')
);
function _saveDismissedWords() {
try { localStorage.setItem('bayan_dismissed_words', JSON.stringify([..._dismissedWords])); } catch {}
}
/**
* MED-2: Whether the analysis results are stale.
* Set to true when the user edits the textarea after analysis.
* When stale, suggestion actions are blocked.
*/
let isStale = false;
const SCORE_CIRCUMFERENCE = 440;
// ══════════════════════════════════════════════════════════
// State persistence — survive popup close/reopen
// ══════════════════════════════════════════════════════════
const STORAGE_KEY = 'bayan_popup_state';
function saveState() {
const activeTab = document.querySelector('.bayan-tab.active');
const state = {
tab: activeTab ? activeTab.dataset.tab : 'correct',
inputText: inputText.value,
summaryInput: summaryInputText.value,
dialectInput: document.getElementById('dialect-input-text')?.value || '',
quranInput: document.getElementById('quran-input-text')?.value || '',
};
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch { /* quota exceeded — ignore */ }
}
function restoreState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return;
const state = JSON.parse(raw);
if (state.inputText) {
inputText.value = state.inputText;
updateCounts(inputText, charCount, wordCount);
}
if (state.summaryInput) {
summaryInputText.value = state.summaryInput;
const words = state.summaryInput.trim() ? state.summaryInput.trim().split(/\s+/).length : 0;
if (summaryWordCountInput) summaryWordCountInput.textContent = words.toLocaleString('ar-EG');
}
const dialectEl = document.getElementById('dialect-input-text');
if (state.dialectInput && dialectEl) dialectEl.value = state.dialectInput;
const quranEl = document.getElementById('quran-input-text');
if (state.quranInput && quranEl) quranEl.value = state.quranInput;
if (state.tab && state.tab !== 'correct') {
const tabBtn = document.querySelector(`.bayan-tab[data-tab="${state.tab}"]`);
if (tabBtn) tabBtn.click();
}
} catch { /* corrupt state — ignore */ }
}
restoreState();
// ══════════════════════════════════════════════════════════
// Tab switching
// ══════════════════════════════════════════════════════════
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}`);
});
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();
// MED-2: Detect user edit after analysis → mark stale
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();
});
// ══════════════════════════════════════════════════════════
// MED-2: Staleness management
// ══════════════════════════════════════════════════════════
/**
* Mark analysis results as stale (user edited textarea).
* Disables suggestion actions and shows a re-analysis prompt.
*/
function markStale() {
if (isStale) return;
isStale = true;
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>
إعادة التحليل`;
}
/**
* Clear staleness (after re-analysis or clear).
*/
function clearStale() {
isStale = false;
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>
تحليل وتصحيح`;
}
// ══════════════════════════════════════════════════════════
// Loading state
// ══════════════════════════════════════════════════════════
function setLoading(show, text = 'جارٍ التحليل...') {
loadingOverlay.classList.toggle('is-hidden', !show);
loadingTextEl.textContent = text;
}
// ══════════════════════════════════════════════════════════
// Toast
// ══════════════════════════════════════════════════════════
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);
}
// ══════════════════════════════════════════════════════════
// 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="bayan-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="bayan-empty-state"><svg class="bayan-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="bayan-empty-state-title">نصك ممتاز!</p><p class="bayan-empty-state-desc">لم نجد أي أخطاء — أحسنت! ✨</p></div>';
} else {
suggestionsSection.classList.add('is-hidden');
}
return;
}
suggestionsSection.classList.remove('is-hidden');
const html = suggestions.map((s, i) => buildSuggestionCardHTML(s, i)).join('');
suggestionsList.innerHTML = html;
// Bind suggestion card events
suggestionsList.querySelectorAll('.bayan-alt-chip').forEach((chip) => {
chip.addEventListener('click', (e) => {
e.stopPropagation();
// MED-2: Block if stale
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) {
// Dismiss — remove from list, add to permanent whitelist
_dismissedWords.add(suggestion.original);
_saveDismissedWords();
currentSuggestions = removeSuggestion(currentSuggestions, suggestion.id);
} else {
// ═══════════════════════════════════════════════════
// HIGH-1 FIX: Apply + Rebase via atomic function
// ═══════════════════════════════════════════════════
const result = applyAndRebase(analyzedText, suggestion, altText, currentSuggestions);
analyzedText = result.text;
currentSuggestions = result.suggestions;
// Sync textarea with the rebased text
inputText.value = analyzedText;
updateCounts(inputText, charCount, wordCount);
}
// Re-render score and suggestions
const counts = countByType(currentSuggestions);
updateScore(counts.spelling, counts.grammar, counts.punctuation);
renderSuggestions(currentSuggestions);
showToast('✓ تم التصحيح');
saveState();
});
});
// 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');
}
}
// ══════════════════════════════════════════════════════════
// Clear button
// ══════════════════════════════════════════════════════════
btnClear.addEventListener('click', () => {
inputText.value = '';
analyzedText = '';
updateCounts(inputText, charCount, wordCount);
scoreSection.classList.add('is-hidden');
suggestionsSection.classList.add('is-hidden');
timingSection.classList.add('is-hidden');
currentSuggestions = [];
clearStale();
saveState();
});
// ══════════════════════════════════════════════════════════
// Correct button
// ══════════════════════════════════════════════════════════
btnCorrect.addEventListener('click', async () => {
const text = inputText.value.trim();
if (!text) {
showToast('أدخل نصاً للتحليل');
return;
}
if (text.trim().split(/\s+/).length < 2) {
showToast('أدخل كلمتين على الأقل');
return;
}
if (text.length > CONFIG.MAX_ANALYZE_LENGTH) {
showToast('النص طويل جداً (الحد الأقصى ٥٠٠٠ حرف)');
return;
}
setLoading(true, 'جارٍ التحليل...');
clearStale(); // MED-2: Clear stale on re-analysis
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;
// MED-2: Snapshot the analyzed text — all offsets reference THIS string
analyzedText = data.original;
// Sync textarea to the exact text the backend analyzed
inputText.value = analyzedText;
updateCounts(inputText, charCount, wordCount);
// Show corrected text in textarea
// (suggestions apply directly to textarea via chips or Apply All)
// Update score
const counts = countByType(suggestions);
updateScore(counts.spelling, counts.grammar, counts.punctuation);
// Render suggestion cards
renderSuggestions(suggestions);
// Show timing
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)`;
}
saveState();
} else {
showToast('تعذّر التحليل — حاول مرة أخرى');
}
} catch (error) {
console.error('[Bayan] Analysis error:', error);
showToast('خطأ في الاتصال — تحقق من الإنترنت');
} finally {
setLoading(false);
}
});
// ══════════════════════════════════════════════════════════
// Apply all button
// ══════════════════════════════════════════════════════════
btnApplyAll.addEventListener('click', () => {
if (currentSuggestions.length === 0) return;
// MED-2: Block if stale
if (isStale) {
showToast('⚠ أعد التحليل أولاً — النص تغيّر');
return;
}
// Apply all patches using reverse-order (no rebase needed — all applied at once)
analyzedText = applyAllPatches(analyzedText, currentSuggestions);
inputText.value = analyzedText;
updateCounts(inputText, charCount, wordCount);
currentSuggestions = [];
updateScore(0, 0, 0);
renderSuggestions([]);
showToast('✓ تم تطبيق جميع التصحيحات');
saveState();
});
// ══════════════════════════════════════════════════════════
// Copy text from textarea (MED-1: .catch() for clipboard errors)
// ══════════════════════════════════════════════════════════
btnCopyText.addEventListener('click', () => {
const text = inputText.value || '';
navigator.clipboard.writeText(text)
.then(() => showToast('✓ تم نسخ النص'))
.catch(() => showToast('تعذّر النسخ'));
});
// ══════════════════════════════════════════════════════════
// Summary mode toggle (paragraph / bullets)
// ══════════════════════════════════════════════════════════
let summaryMode = 'paragraph';
document.querySelectorAll('.bayan-mode-btn').forEach((btn) => {
btn.addEventListener('click', () => {
summaryMode = btn.dataset.mode;
document.querySelectorAll('.bayan-mode-btn').forEach((b) => {
b.classList.toggle('active', b.dataset.mode === summaryMode);
});
});
});
// ══════════════════════════════════════════════════════════
// Summarize button
// ══════════════════════════════════════════════════════════
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] Summarization error:', error);
showToast('خطأ في الاتصال — تحقق من الإنترنت');
} finally {
setLoading(false);
}
});
// ══════════════════════════════════════════════════════════
// Copy summary (MED-1: .catch() for clipboard errors)
// ══════════════════════════════════════════════════════════
btnCopySummary.addEventListener('click', () => {
const text = summaryText.innerText || summaryText.textContent || '';
navigator.clipboard.writeText(text)
.then(() => showToast('✓ تم نسخ الملخص'))
.catch(() => showToast('تعذّر النسخ'));
});
// ══════════════════════════════════════════════════════════
// 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] Dialect error:', error);
showToast('خطأ في الاتصال — تحقق من الإنترنت');
} finally {
setLoading(false);
}
});
btnCopyDialect.addEventListener('click', () => {
navigator.clipboard.writeText(dialectText.textContent || '')
.then(() => showToast('✓ تم نسخ النص'))
.catch(() => showToast('تعذّر النسخ'));
});
}
// ══════════════════════════════════════════════════════════
// Quran verification + translation (matching website)
// ══════════════════════════════════════════════════════════
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 = 'bayan-quran-match-card';
card.innerHTML = '<div class="bayan-quran-result-header">'
+ '<span style="color:#06b6d4;font-size:12px;font-weight:700;">✓ نتيجة ' + (i + 1) + '</span>'
+ '<button class="bayan-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>'
+ '<p class="bayan-quran-uthmani" dir="rtl">' + _escHTML(parsed.verseText) + '</p>'
+ '<p class="bayan-quran-reference">' + (parsed.reference ? '[' + _escHTML(parsed.reference) + ']' : '') + '</p>'
+ '<div class="bayan-quran-translate">'
+ '<div class="bayan-quran-translate-row">'
+ '<span style="font-size:12px;font-weight:600;color:var(--text-secondary);">ترجمة الآية</span>'
+ '<select class="bayan-quran-lang-select qmc-lang">' + _langOptionsHTML() + '</select>'
+ '</div>'
+ '<div class="bayan-quran-translation is-hidden qmc-trans-section">'
+ '<p class="qmc-trans-text" dir="auto"></p>'
+ '<button class="bayan-btn-icon qmc-copy-trans" type="button" title="نسخ الترجمة" style="margin-top:4px;">'
+ '<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>';
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('تعذّر النسخ'); });
});
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');
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('تعذّر النسخ'); });
});
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] 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);
}
});
}
// ══════════════════════════════════════════════════════════
// 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('.bayan-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);
}
// ── Close button: close the popup window ──
const btnClosePopup = document.getElementById('btn-close-popup');
if (btnClosePopup) {
btnClosePopup.addEventListener('click', () => window.close());
}
// ══════════════════════════════════════════════════════════
// Status check on load
// ══════════════════════════════════════════════════════════
(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 = 'غير متصل';
}
})();
// ══════════════════════════════════════════════════════════
// Phase 4: Context Menu Pickup
// ══════════════════════════════════════════════════════════
// Fix #2: Guard against double execution
// Fix #4: Tab name constants
// Fix #5: Storage fallback (session → local)
// ══════════════════════════════════════════════════════════
const TAB_ACTIONS = { correct: 'correct', summarize: 'summarize' };
let contextConsumed = false; // Fix #2
(async function checkContextAction() {
// Only available in extension context (not in test pages)
if (typeof chrome === 'undefined' || !chrome.storage) return;
// Fix #2: Prevent double-trigger
if (contextConsumed) return;
// Fix #5: Storage fallback
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;
// Ignore stale actions (older than 15 seconds — matches background cleanup)
const age = Date.now() - (data.contextTimestamp || 0);
if (age > 15000) {
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
return;
}
// Fix #2: Mark as consumed BEFORE processing
contextConsumed = true;
console.log(`[Bayan] Context action: ${data.contextAction}, text length: ${data.contextText.length}`);
if (data.contextAction === TAB_ACTIONS.correct) {
// Fill the correction tab and auto-trigger analysis
inputText.value = data.contextText;
updateCounts(inputText, charCount, wordCount);
// Switch to correction tab
const correctTab = document.querySelector(`[data-tab="${TAB_ACTIONS.correct}"]`);
if (correctTab) correctTab.click();
// Auto-click the correct button after a brief delay for UI paint
setTimeout(() => btnCorrect.click(), 150);
} else if (data.contextAction === TAB_ACTIONS.summarize) {
// Fill the summary tab and auto-trigger summarization
summaryInputText.value = data.contextText;
const ctxWords = data.contextText.trim() ? data.contextText.trim().split(/\s+/).length : 0;
if (summaryWordCountInput) summaryWordCountInput.textContent = ctxWords.toLocaleString('ar-EG');
// Switch to summarize tab
const summarizeTab = document.querySelector(`[data-tab="${TAB_ACTIONS.summarize}"]`);
if (summarizeTab) summarizeTab.click();
// Auto-click the summarize button
setTimeout(() => btnSummarize.click(), 150);
}
// Clear the context action so it doesn't re-trigger on next popup open
chrome.runtime.sendMessage({ type: 'CLEAR_CONTEXT' });
} catch (err) {
console.warn('[Bayan] Context action check failed:', err);
}
})();
});
// ── Theme Toggle Logic ──
(function initBayanThemeToggle() {
const toggleBtn = document.getElementById('ext-theme-toggle');
// Load theme from storage
chrome.storage.local.get(['theme'], (result) => {
const currentTheme = result.theme || 'dark'; // default to dark
document.documentElement.setAttribute('data-theme', currentTheme);
});
// Sync theme changes instantly across all views
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 });
});
}
})(); |