// 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');
}
}