// Bayan UI helpers — score, suggestions list, mobile nav, bottom sheet const TYPE_LABELS = { spelling: 'إملائي', grammar: 'نحوي', punctuation: 'ترقيم' }; const SCORE_CIRCUMFERENCE = 440; /** * Calculate writing score from suggestion counts */ function calculateWritingScore(spelling, grammar, punctuation) { const score = 100 - spelling * 8 - grammar * 6 - punctuation * 3; return Math.max(0, Math.min(100, score)); } /** * Update the score ring UI */ function updateWritingScore(spelling, grammar, punctuation) { const score = calculateWritingScore(spelling, grammar, punctuation); const valueEl = document.getElementById('score-value'); const circleEl = document.getElementById('score-circle'); const hintEl = document.getElementById('score-hint'); if (valueEl) { valueEl.textContent = score > 0 || (spelling + grammar + punctuation) > 0 ? score.toLocaleString('ar-EG') : '--'; } if (circleEl) { const offset = SCORE_CIRCUMFERENCE - (score / 100) * SCORE_CIRCUMFERENCE; circleEl.style.strokeDashoffset = String(offset); } if (hintEl) { const total = spelling + grammar + punctuation; if (total === 0) { hintEl.innerHTML = 'ابدأ الكتابة لرؤية تقييمك
تحسين القواعد يرفع التقييم'; } else if (score >= 90) { hintEl.textContent = 'كتابة ممتازة! استمر.'; } else if (score >= 70) { hintEl.textContent = 'جيد — راجع الاقتراحات لتحسين النص.'; } else { hintEl.textContent = 'يحتاج النص إلى بعض التحسينات.'; } } const sheetCount = document.getElementById('mobile-suggestion-count'); if (sheetCount) { const total = spelling + grammar + punctuation; sheetCount.textContent = total.toLocaleString('ar-EG'); } } /** * Build HTML for a single suggestion card */ /** * Resolve alternatives for a suggestion, falling back to [correction, original] * when the model doesn't provide alternatives (e.g. grammar, punctuation). * Shared logic — must stay in sync with tooltip rendering in editor.js. */ function resolveAlternatives(suggestion) { return (suggestion.alternatives && suggestion.alternatives.length > 0) ? suggestion.alternatives : [suggestion.correction, suggestion.original]; } function buildSuggestionCardHTML(suggestion, index) { const badgeClass = `badge-${suggestion.type}`; const label = TYPE_LABELS[suggestion.type] || suggestion.type; const alts = resolveAlternatives(suggestion); // Pipeline Hardening v3.3: Use suggestion.id (UUID) instead of array index const suggestionId = suggestion.id || index; let altsHTML = ''; alts.forEach((alt, i) => { const isKeep = alt === suggestion.original; const isMain = i === 0; const cls = isKeep ? 'alt-chip alt-chip--keep' : (isMain ? 'alt-chip alt-chip--main' : 'alt-chip'); const chipLabel = isKeep ? `${escapeHtml(alt)} ✓` : escapeHtml(alt); altsHTML += ``; }); return `
${label}
${escapeHtml(suggestion.original)}
${altsHTML}
`; } /** * Render suggestions into sidebar and bottom sheet lists */ function updateSuggestionsList(suggestions) { const lists = [ document.getElementById('suggestions-list'), document.getElementById('bottom-sheet-list') ].filter(Boolean); const applyAllBtn = document.getElementById('apply-all-btn'); const applyAllSheet = document.getElementById('apply-all-sheet'); if (!suggestions || suggestions.length === 0) { const editorText = typeof getEditorText === 'function' ? getEditorText().trim() : ''; const hasText = editorText.length > 0; const emptyHTML = `
${hasText ? 'نصك ممتاز!' : 'لا توجد اقتراحات'}
${hasText ? 'لم نجد أي أخطاء — أحسنت! ✨' : 'ابدأ بكتابة نص عربي وسيتم تحليله تلقائياً'}
`; lists.forEach((el) => { el.innerHTML = emptyHTML; }); if (applyAllBtn) applyAllBtn.classList.add('is-hidden'); if (applyAllSheet) applyAllSheet.classList.add('is-hidden'); return; } const cardsHTML = suggestions.map((s, i) => buildSuggestionCardHTML(s, i)).join(''); lists.forEach((el) => { el.innerHTML = cardsHTML; bindSuggestionCardEvents(el); }); const showApplyAll = suggestions.length >= 2; const countLabel = suggestions.length.toLocaleString('ar-EG'); if (applyAllBtn) { applyAllBtn.classList.toggle('is-hidden', !showApplyAll); if (showApplyAll) applyAllBtn.textContent = '\u062a\u0637\u0628\u064a\u0642 \u0627\u0644\u0643\u0644 (' + countLabel + ')'; } if (applyAllSheet) { applyAllSheet.classList.toggle('is-hidden', !showApplyAll); if (showApplyAll) applyAllSheet.textContent = '\u062a\u0637\u0628\u064a\u0642 \u0627\u0644\u0643\u0644 (' + countLabel + ')'; } } function bindSuggestionCardEvents(container) { container.querySelectorAll('.suggestion-card').forEach((card) => { card.addEventListener('click', (e) => { if (e.target.closest('.alt-chip') || e.target.closest('.suggestion-card-apply')) return; // Pipeline Hardening v3.3: UUID-based lookup const suggestionId = card.dataset.suggestionId; scrollToSuggestion(suggestionId); focusSuggestionInEditor(suggestionId); }); card.addEventListener('keydown', (e) => { if (e.key === 'Enter') { const suggestionId = card.dataset.suggestionId; applySuggestionById(suggestionId); } }); }); // Alt chip clicks container.querySelectorAll('.alt-chip').forEach((chip) => { chip.addEventListener('click', (e) => { e.stopPropagation(); // Pipeline Hardening v3.3: UUID-based lookup const suggestionId = chip.dataset.cardId; const altText = chip.dataset.cardAlt; const suggestion = findSuggestionById(suggestionId); if (!suggestion) return; if (altText === suggestion.original) { dismissSuggestion(suggestion); } else { applyAlternativeCorrection(suggestion, altText); } }); }); container.querySelectorAll('.suggestion-card-apply').forEach((btn) => { btn.addEventListener('click', (e) => { e.stopPropagation(); const suggestionId = btn.dataset.applyId; applySuggestionById(suggestionId); }); }); } function scrollToSuggestion(suggestionId) { // Pipeline Hardening v3.3: UUID-based span lookup const span = document.querySelector(`[data-suggestion-id="${suggestionId}"]`); if (span) { span.scrollIntoView({ behavior: 'smooth', block: 'center' }); span.classList.add('highlight-active'); setTimeout(() => span.classList.remove('highlight-active'), 1500); showTooltip(span); } } function focusSuggestionInEditor(suggestionId) { const span = document.querySelector(`[data-suggestion-id="${suggestionId}"]`); if (span) showTooltip(span); } let _analyzingTimer = null; function setAnalyzingState(isAnalyzing) { const editor = getEditorElement(); const indicator = document.getElementById('analyzing-indicator'); if (editor) { editor.setAttribute('aria-busy', isAnalyzing ? 'true' : 'false'); } if (isAnalyzing) { // Debounce: only show indicator after 400ms to prevent flicker on fast re-analyses if (!_analyzingTimer) { _analyzingTimer = setTimeout(() => { if (editor) editor.classList.add('analyzing'); if (indicator) indicator.classList.add('active'); // Show skeleton loading in suggestions panel const lists = [ document.getElementById('suggestions-list'), document.getElementById('bottom-sheet-list') ].filter(Boolean); const skeletonHTML = `
`; lists.forEach(el => { el.innerHTML = skeletonHTML; }); }, 400); } } else { // Clear timer and hide immediately if (_analyzingTimer) { clearTimeout(_analyzingTimer); _analyzingTimer = null; } if (editor) editor.classList.remove('analyzing'); if (indicator) indicator.classList.remove('active'); } } /* ── Mobile navigation ── */ function initMobileNav() { const btn = document.getElementById('mobile-menu-btn'); const drawer = document.getElementById('mobile-drawer'); const backdrop = document.getElementById('mobile-drawer-backdrop'); const closeBtn = document.getElementById('mobile-drawer-close'); if (!btn || !drawer) return; function openDrawer() { drawer.classList.add('open'); btn.setAttribute('aria-expanded', 'true'); document.body.style.overflow = 'hidden'; } function closeDrawer() { drawer.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); document.body.style.overflow = ''; } btn.addEventListener('click', openDrawer); if (backdrop) backdrop.addEventListener('click', closeDrawer); if (closeBtn) closeBtn.addEventListener('click', closeDrawer); drawer.querySelectorAll('.mobile-drawer-link').forEach((link) => { link.addEventListener('click', () => { closeDrawer(); const page = link.dataset.page; if (page) showPage(page); }); }); } /* ── Bottom sheet ── */ function initBottomSheet() { const trigger = document.getElementById('mobile-sheet-trigger'); const sheet = document.getElementById('bottom-sheet'); const backdrop = document.getElementById('bottom-sheet-backdrop'); const closeBtn = document.getElementById('bottom-sheet-close'); if (!trigger || !sheet) return; function openSheet() { sheet.classList.add('open'); trigger.setAttribute('aria-expanded', 'true'); } function closeSheet() { sheet.classList.remove('open'); trigger.setAttribute('aria-expanded', 'false'); } trigger.addEventListener('click', openSheet); if (backdrop) backdrop.addEventListener('click', closeSheet); if (closeBtn) closeBtn.addEventListener('click', closeSheet); } function initUI() { initMobileNav(); initBottomSheet(); updateWritingScore(0, 0, 0); } /** * Show non-blocking document operation toast * @param {string} message * @param {'success'|'error'|'info'} type */ function showDocToast(message, type = 'info') { let toast = document.getElementById('doc-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'doc-toast'; toast.className = 'doc-toast'; toast.setAttribute('role', 'status'); toast.setAttribute('aria-live', 'polite'); document.body.appendChild(toast); } toast.textContent = message; toast.className = `doc-toast doc-toast--${type} is-visible`; clearTimeout(toast._hideTimer); toast._hideTimer = setTimeout(() => toast.classList.remove('is-visible'), 3500); } /** * Show/hide analysis length warning banner * @param {boolean} show */ function updateAnalysisLimitBanner(show) { const banner = document.getElementById('analysis-limit-banner'); if (!banner) return; if (show) { banner.classList.remove('is-hidden'); banner.textContent = 'النص أطول من الحد المسموح للتحليل. سيتم تحليل أول 5000 حرف فقط.'; } else { banner.classList.add('is-hidden'); } }