(function () { // Theme toggle initialization const themeToggle = document.getElementById("theme-toggle"); if (themeToggle) { const savedTheme = localStorage.getItem("theme") || "light"; if (savedTheme === "dark") { document.body.classList.add("dark-theme"); themeToggle.querySelector(".theme-icon").textContent = "☀️"; themeToggle.querySelector(".theme-text").textContent = "المظهر المضيء"; } themeToggle.addEventListener("click", () => { document.body.classList.toggle("dark-theme"); const isDark = document.body.classList.contains("dark-theme"); localStorage.setItem("theme", isDark ? "dark" : "light"); themeToggle.querySelector(".theme-icon").textContent = isDark ? "\u2600" : "\u263D"; themeToggle.querySelector(".theme-text").textContent = isDark ? "المظهر المضيء" : "المظهر الداكن"; }); } const form = document.getElementById("search-form"); const submitBtn = document.getElementById("submit-btn"); const btnText = document.getElementById("btn-text"); const btnSpinner = document.getElementById("btn-spinner"); const progressSection = document.getElementById("progress-section"); const searchPhase = document.getElementById("search-phase"); const writePhase = document.getElementById("write-phase"); const writeBtn = document.getElementById("write-btn"); const writeBtnText = document.getElementById("write-btn-text"); const writeSpinner = document.getElementById("write-spinner"); const writeInfo = document.getElementById("write-info"); const writeProgressSection = document.getElementById("write-progress-section"); const progressFill = document.getElementById("progress-fill"); const writeFill = document.getElementById("write-fill"); const progressPercent = document.getElementById("progress-percent"); const writePercent = document.getElementById("write-percent"); const statusText = document.getElementById("status-text"); const writeStatusText = document.getElementById("write-status-text"); const logContent = document.getElementById("log-content"); const sectionCards = document.getElementById("section-cards"); const resultsSection = document.getElementById("results-section"); const resultTitle = document.getElementById("result-title"); const summary = document.getElementById("summary"); const sectionDetail = document.getElementById("section-detail"); const filePaths = document.getElementById("file-paths"); const reviewPhase = document.getElementById("review-phase"); const reviewBtn = document.getElementById("review-btn"); const reviewBtnText = document.getElementById("review-btn-text"); const reviewSpinner = document.getElementById("review-spinner"); const reviewProgressSection = document.getElementById("review-progress-section"); const reviewFill = document.getElementById("review-fill"); const reviewPercent = document.getElementById("review-percent"); const reviewStatusText = document.getElementById("review-status-text"); const studyPreview = document.getElementById("study-preview"); const studyContent = document.getElementById("study-content"); const previewActions = document.getElementById("preview-actions"); const rawPathRow = document.getElementById("raw-path-row"); const rawPathEl = document.getElementById("raw-path"); const studyPathRow = document.getElementById("study-path-row"); const studyPathEl = document.getElementById("study-path"); const errorSection = document.getElementById("error-section"); const errorText = document.getElementById("error-text"); const restoreNotice = document.getElementById("restore-notice"); const downloadSuperPromptBtn = document.getElementById("download-super-prompt-btn"); const copySuperPromptBtn = document.getElementById("copy-super-prompt-btn"); const copyAbsPathBtn = document.getElementById("copy-abs-path-btn"); if (copyAbsPathBtn) { copyAbsPathBtn.addEventListener("click", function () { var pathVal = document.getElementById("folder-abs-path")?.textContent || ""; if (pathVal) { navigator.clipboard.writeText(pathVal).then(function () { var origText = copyAbsBtn.textContent; copyAbsPathBtn.textContent = "✅ تم النسخ!"; setTimeout(function () { copyAbsPathBtn.textContent = origText; }, 2000); }); } }); } const importText = document.getElementById("import-text"); const importBtn = document.getElementById("import-btn"); const importBtnText = document.getElementById("import-btn-text"); const importSpinner = document.getElementById("import-spinner"); const manualWriteActions = document.getElementById("manual-write-actions"); const newSessionBtn = document.getElementById("new-session-btn"); const cancelSearchBtn = document.getElementById("cancel-search-btn"); const auditSection = document.getElementById("audit-section"); const auditBadgeContainer = document.getElementById("audit-badge-container"); const auditStatsGrid = document.getElementById("audit-stats-grid"); const auditIssuesList = document.getElementById("audit-issues-list"); const pastJobsList = document.getElementById("past-jobs-list"); let currentJobId = null; let currentStudyFullText = null; let currentStudyRenderedLength = 0; var LOAD_MORE_CHUNK = 4000; // C6: API key override from UI let apiKeyOverride = ""; const apiKeyInput = document.getElementById("api-key-input"); if (apiKeyInput) { apiKeyInput.addEventListener("change", function() { apiKeyOverride = apiKeyInput.value.trim(); }); } const STORAGE_KEY = "alfaz_job_id"; const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ""; async function loadPastJobs() { if (!pastJobsList) return; // Show skeleton while loading pastJobsList.innerHTML = '
' + '
' + '
'; try { var resp = await fetch("/flask/jobs"); var data = await resp.json(); if (!data || data.length === 0) { pastJobsList.innerHTML = '

لا توجد جلسات نشطة حالياً.

'; return; } var html = ""; data.forEach(function (job) { var jobClass = "job-item" + (job.done ? " completed" : ""); var dateStr = ""; if (job.created_at) { var date = new Date(job.created_at * 1000); dateStr = date.toLocaleTimeString("ar-EG", { hour: '2-digit', minute: '2-digit' }) + " " + date.toLocaleDateString("ar-EG", { month: 'short', day: 'numeric' }); } var statusLabel = job.status || (job.done ? "مكتمل" : "جاري العمل..."); html += '
' + '' + esc(job.term) + ' (' + (job.category === "tadeel" ? "تعديل" : "جرح") + ')' + '' + esc(statusLabel) + '' + '' + esc(dateStr) + '' + '' + '
'; }); pastJobsList.innerHTML = html; pastJobsList.querySelectorAll(".job-item").forEach(function (item) { item.addEventListener("click", function (e) { if (e.target.classList.contains("job-delete-btn")) return; var jid = item.getAttribute("data-id"); loadJobDetails(jid); }); }); pastJobsList.querySelectorAll(".job-delete-btn").forEach(function (btn) { btn.addEventListener("click", function (e) { e.stopPropagation(); var jid = btn.getAttribute("data-id"); deleteJob(jid); }); }); } catch (e) { console.error("Failed to load past jobs", e); } } var completedList = document.getElementById("completed-studies-list"); async function loadCompletedStudies() { if (!completedList) return; completedList.innerHTML = '
' + '
'; try { var resp = await fetch("/flask/completed"); var data = await resp.json(); if (!data || data.length === 0) { completedList.innerHTML = '

لا توجد دراسات مكتملة بعد.

'; return; } var html = ""; data.forEach(function (s) { var statusClass = s.status === "مكتمل" ? "status-done" : s.status === "يحتاج مراجعة" ? "status-warn" : "status-pending"; var dlBtns = ""; if (s.study_dl) { dlBtns += 'MD'; } if (s.docx_dl) { dlBtns += 'Word'; } var meta = s.word_count ? s.word_count + " كلمة" : ""; if (s.total_results) meta += (meta ? " · " : "") + s.total_results + " نتيجة"; var clickable = ' style="cursor:pointer"'; // Store the study path for fetching full text on click (not the truncated text) var dataAttrs = ' data-term="' + esc(s.term) + '" data-cat="' + esc(s.category_key) + '"' + ' data-job-id="' + esc(s.job_id || "") + '"' + ' data-study-path="' + esc(s.study_path || "") + '"' + ' data-study-dl="' + esc(s.study_dl || "") + '"' + ' data-docx-dl="' + esc(s.docx_dl || "") + '"' + ' data-status="' + esc(s.status || "") + '"'; html += '
' + '
' + '' + esc(s.term) + '' + '' + esc(s.category) + '' + '
' + '' + esc(s.status) + '' + (meta ? '' + esc(meta) + '' : '') + (dlBtns ? '
' + dlBtns + '
' : '') + '
'; }); completedList.innerHTML = html; // Click handler: fetch full study text via /study-by-path, or load job details completedList.querySelectorAll(".completed-item[data-term]").forEach(function (item) { item.addEventListener("click", function (e) { if (e.target.tagName === "A") return; // Don't intercept download link clicks var term = item.getAttribute("data-term"); var cat = item.getAttribute("data-cat"); var jid = item.getAttribute("data-job-id") || ""; var studyPath = item.getAttribute("data-study-path") || ""; var studyDl = item.getAttribute("data-study-dl") || ""; if (studyPath) { // Show loading state in modal immediately openStudyModal("⏳ جاري تحميل الدراسة...", term + " (" + cat + ")"); // Fetch full text from server fetch("/flask/study-by-path?path=" + encodeURIComponent(studyPath)) .then(function(r) { return r.json(); }) .then(function(res) { if (res.error) { openStudyModal("❌ خطأ: " + res.error, term + " (" + cat + ")"); } else { openStudyModal(res.study_text || "", term + " (" + cat + ")"); } }) .catch(function(err) { openStudyModal("❌ فشل تحميل الدراسة: " + err.message, term + " (" + cat + ")"); }); } else if (jid) { // Load job to display the 'write study' phase directly loadJobDetails(jid); } else if (studyDl) { // Fallback: open download link window.open(studyDl, "_blank"); } else { // No study yet: fill form to start search document.getElementById("term").value = term; document.getElementById("category").value = cat; document.getElementById("search-form").scrollIntoView({ behavior: "smooth" }); document.getElementById("term").focus(); } }); }); } catch (e) { completedList.innerHTML = '

فشل تحميل الدراسات.

'; console.error("Failed to load completed studies", e); } } async function deleteJob(jobId) { if (!confirm("هل أنت متأكد من حذف هذه الجلسة؟")) return; try { var resp = await fetch("/flask/delete/" + jobId, { method: "DELETE", headers: { "X-CSRFToken": csrfToken } }); var data = await resp.json(); if (data.error) { showError(data.error); return; } if (currentJobId === jobId) { currentJobId = null; clearJobId(); progressSection.classList.add("hidden"); searchPhase.classList.add("hidden"); writePhase.classList.add("hidden"); reviewPhase.classList.add("hidden"); resultsSection.classList.add("hidden"); studyPreview.classList.add("hidden"); filePaths.classList.add("hidden"); errorSection.classList.add("hidden"); submitBtn.disabled = false; btnText.classList.remove("hidden"); btnSpinner.classList.add("hidden"); } loadPastJobs(); } catch (e) { showError("فشل حذف الجلسة: " + e.message); } } function loadJobDetails(jid) { disconnectSSE(); fetch("/flask/status/" + jid) .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); }) .then(function (data) { if (data.error) { showError(data.error); return; } currentJobId = jid; saveJobId(jid); // Reset and hide all views before restoring progressSection.classList.add("hidden"); searchPhase.classList.add("hidden"); writePhase.classList.add("hidden"); reviewPhase.classList.add("hidden"); resultsSection.classList.add("hidden"); studyPreview.classList.add("hidden"); filePaths.classList.add("hidden"); restoreNotice.classList.add("hidden"); errorSection.classList.add("hidden"); restoreUI(data); }) .catch(function (e) { showError("هذه الجلسة لم تعد متاحة. ابدأ بحثاً جديداً."); clearJobId(); currentJobId = null; loadPastJobs(); }); } function saveJobId(id) { try { localStorage.setItem(STORAGE_KEY, id); } catch (e) {} } function clearJobId() { try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} } function loadSavedJob() { var saved = null; try { saved = localStorage.getItem(STORAGE_KEY); } catch (e) {} if (!saved) return; fetch("/flask/status/" + saved) .then(function (r) { return r.json(); }) .then(function (data) { if (data.error) { clearJobId(); return; } currentJobId = saved; restoreUI(data); }) .catch(function () { clearJobId(); }); } function restoreUI(data) { progressSection.classList.remove("hidden"); progressSection.classList.add("fade-in"); restoreNotice.classList.remove("hidden"); if (data.search_done) { onSearchComplete(data, true); if (data.write_done && !data.write_error) { onWriteComplete(data, true); if (data.review_in_progress) { reviewPhase.classList.remove("hidden"); reviewPhase.classList.add("fade-in"); reviewProgressSection.classList.remove("hidden"); reviewBtn.disabled = true; reviewStatusText.textContent = data.status || "جاري المراجعة..."; reviewFill.style.width = (data.review_progress || 0) + "%"; reviewPercent.textContent = (data.review_progress || 0) + "%"; connectSSE(currentJobId); } else if (data.review_done) { onReviewComplete(data); } } else if (data.write_error) { writePhase.classList.remove("hidden"); writePhase.classList.add("fade-in"); if (manualWriteActions) { manualWriteActions.classList.add("hidden"); } writeProgressSection.classList.add("hidden"); writeBtn.disabled = false; writeBtnText.textContent = "✍️ اكتب الدراسة (إعادة محاولة)"; writeSpinner.classList.add("hidden"); showError("حدث خطأ سابق أثناء الكتابة: " + data.write_error); } else if (data.write_progress > 0) { writePhase.classList.remove("hidden"); writePhase.classList.add("fade-in"); if (manualWriteActions) { manualWriteActions.classList.add("hidden"); } writeProgressSection.classList.remove("hidden"); writeBtn.disabled = true; writeStatusText.textContent = data.status || "جاري الكتابة..."; writeFill.style.width = (data.write_progress || 0) + "%"; writePercent.textContent = (data.write_progress || 0) + "%"; connectSSE(currentJobId); } else { // Search is done, ready for writing study writePhase.classList.remove("hidden"); writePhase.classList.add("fade-in"); writeBtn.disabled = false; writeBtnText.textContent = "✍️ اكتب الدراسة"; writeSpinner.classList.add("hidden"); writeProgressSection.classList.add("hidden"); } } else if (data.search_progress > 0) { searchPhase.classList.remove("hidden"); searchPhase.classList.add("fade-in"); updateSearchUI(data); connectSSE(currentJobId); } } document.querySelectorAll(".section-card").forEach(function (card) { card.addEventListener("click", function () { document.querySelectorAll(".section-card").forEach(function (c) { c.classList.remove("active"); }); card.classList.add("active"); }); }); form.addEventListener("submit", async function (e) { e.preventDefault(); var term = document.getElementById("term").value.trim(); if (!term) return; submitBtn.disabled = true; btnText.classList.add("hidden"); btnSpinner.classList.remove("hidden"); var category = document.getElementById("category").value; var depth = document.getElementById("depth").value; var section = document.querySelector('input[name="section"]:checked') ? document.querySelector('input[name="section"]:checked').value : "all"; var sources = ["shamela"]; progressSection.classList.remove("hidden"); progressSection.classList.add("fade-in"); searchPhase.classList.remove("hidden"); searchPhase.classList.add("fade-in"); writePhase.classList.add("hidden"); resultsSection.classList.add("hidden"); errorSection.classList.add("hidden"); studyPreview.classList.add("hidden"); filePaths.classList.add("hidden"); restoreNotice.classList.add("hidden"); try { var resp = await fetch("/flask/start", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRFToken": csrfToken }, body: JSON.stringify({ term: term, category: category, mode: depth, section: section, sources: sources, api_key: apiKeyOverride || undefined, }), }); var data = await resp.json(); if (data.error) { showError(data.error); submitBtn.disabled = false; btnText.classList.remove("hidden"); btnSpinner.classList.add("hidden"); return; } currentJobId = data.job_id; saveJobId(data.job_id); loadPastJobs(); connectSSE(data.job_id); if (cancelSearchBtn) { cancelSearchBtn.classList.remove("hidden"); } } catch (err) { showError("فشل الاتصال بالخادم: " + err.message); submitBtn.disabled = false; btnText.classList.remove("hidden"); btnSpinner.classList.add("hidden"); } }); writeBtn.addEventListener("click", async function () { if (!currentJobId) return; writeBtn.disabled = true; writeBtnText.textContent = "جاري الكتابة..."; writeSpinner.classList.remove("hidden"); writeProgressSection.classList.remove("hidden"); writeFill.style.width = "0%"; writePercent.textContent = "0%"; writeStatusText.textContent = "جاري بدء الكتابة..."; if (manualWriteActions) { manualWriteActions.classList.add("hidden"); } try { var body = {}; if (apiKeyOverride) body.api_key = apiKeyOverride; var resp = await fetch("/flask/write/" + currentJobId, { method: "POST", headers: { "X-CSRFToken": csrfToken, "Content-Type": "application/json" }, body: JSON.stringify(body), }); var data = await resp.json(); if (data.error) { showError(data.error); writeBtn.disabled = false; writeBtnText.textContent = "✍️ اكتب الدراسة"; writeSpinner.classList.add("hidden"); writeProgressSection.classList.add("hidden"); if (manualWriteActions) { manualWriteActions.classList.remove("hidden"); } return; } loadPastJobs(); connectSSE(currentJobId); } catch (err) { showError("فشل بدء الكتابة: " + err.message); writeBtn.disabled = false; writeBtnText.textContent = "✍️ اكتب الدراسة"; writeSpinner.classList.add("hidden"); writeProgressSection.classList.add("hidden"); if (manualWriteActions) { manualWriteActions.classList.remove("hidden"); } } }); if (importBtn) { importBtn.addEventListener("click", async function () { if (!currentJobId) return; var text = importText.value.trim(); if (!text || text.length < 100) { alert("الرجاء إلصاق نص الدراسة كاملاً أولاً (100 حرف على الأقل)."); return; } importBtn.disabled = true; importSpinner.classList.remove("hidden"); importBtnText.textContent = "جاري استيراد ومعالجة الدراسة..."; try { var resp = await fetch("/flask/import/" + currentJobId, { method: "POST", headers: { "Content-Type": "application/json", "X-CSRFToken": csrfToken }, body: JSON.stringify({ text: text }) }); var res = await resp.json(); if (res.error) { alert("خطأ: " + res.error); } else { importText.value = ""; loadJobDetails(currentJobId); loadPastJobs(); } } catch (err) { alert("فشل الاتصال بالخادم: " + err.message); } finally { importBtn.disabled = false; importSpinner.classList.add("hidden"); importBtnText.textContent = "حفظ واستيراد الدراسة اليدوية"; } }); } if (newSessionBtn) { newSessionBtn.addEventListener("click", function () { clearJobId(); currentJobId = null; // Reset form document.getElementById("term").value = ""; // Reset UI views progressSection.classList.add("hidden"); searchPhase.classList.add("hidden"); writePhase.classList.add("hidden"); reviewPhase.classList.add("hidden"); resultsSection.classList.add("hidden"); studyPreview.classList.add("hidden"); filePaths.classList.add("hidden"); restoreNotice.classList.add("hidden"); errorSection.classList.add("hidden"); // Reset progress bars and status progressFill.style.width = "0%"; progressPercent.textContent = "0%"; statusText.textContent = "جاري التجهيز..."; logContent.innerHTML = ""; sectionCards.innerHTML = ""; writeFill.style.width = "0%"; writePercent.textContent = "0%"; writeStatusText.textContent = "جاري الكتابة..."; // Enable search submit submitBtn.disabled = false; btnText.classList.remove("hidden"); btnSpinner.classList.add("hidden"); loadPastJobs(); }); } if (cancelSearchBtn) { cancelSearchBtn.addEventListener("click", async function () { if (!currentJobId) return; if (!confirm("هل تريد إلغاء البحث؟ يمكنك تعديل المعاملات وإعادة التشغيل.")) return; cancelSearchBtn.disabled = true; cancelSearchBtn.textContent = "جاري الإلغاء..."; try { await fetch("/flask/cancel/" + currentJobId, { method: "POST", headers: { "X-CSRFToken": csrfToken }, }); } catch (e) { // Ignore network errors — job may already be cleaned up } // Reset UI disconnectSSE(); currentJobId = null; clearJobId(); progressSection.classList.add("hidden"); searchPhase.classList.add("hidden"); writePhase.classList.add("hidden"); reviewPhase.classList.add("hidden"); resultsSection.classList.add("hidden"); studyPreview.classList.add("hidden"); filePaths.classList.add("hidden"); errorSection.classList.add("hidden"); restoreNotice.classList.add("hidden"); progressFill.style.width = "0%"; progressPercent.textContent = "0%"; statusText.textContent = "جاري التجهيز..."; logContent.innerHTML = ""; sectionCards.innerHTML = ""; submitBtn.disabled = false; btnText.classList.remove("hidden"); btnSpinner.classList.add("hidden"); cancelSearchBtn.classList.add("hidden"); cancelSearchBtn.disabled = false; cancelSearchBtn.textContent = "إلغاء"; loadPastJobs(); }); } // ── SSE (Server-Sent Events) — replaces polling ──────────────── var currentEventSource = null; function connectSSE(jobId) { if (currentEventSource) { currentEventSource.close(); } var es = new EventSource("/flask/stream/" + jobId); currentEventSource = es; es.addEventListener("init", function (e) { try { var data = JSON.parse(e.data); updateSearchUI(data); if (data.search_done) { onSearchComplete(data, true); } if (data.write_done) { onWriteComplete(data, true); } } catch (err) {} }); es.addEventListener("search_progress", function (e) { try { var data = JSON.parse(e.data); statusText.textContent = data.status; progressFill.style.width = data.progress + "%"; progressPercent.textContent = data.progress + "%"; if (data.log) { appendLog(data.log); } } catch (err) {} }); es.addEventListener("search_done", function (e) { try { var data = JSON.parse(e.data); onSearchComplete(data); loadPastJobs(); } catch (err) {} }); es.addEventListener("write_progress", function (e) { try { var data = JSON.parse(e.data); writeStatusText.textContent = data.status; writeFill.style.width = data.progress + "%"; writePercent.textContent = data.progress + "%"; if (data.log) { appendLog(data.log); } } catch (err) {} }); es.addEventListener("write_done", function (e) { try { var data = JSON.parse(e.data); if (data.write_error) { showError(data.write_error); writeBtn.disabled = false; writeBtnText.textContent = "إعادة المحاولة"; writeSpinner.classList.add("hidden"); } else { onWriteComplete(data); } loadPastJobs(); } catch (err) {} }); es.addEventListener("review_progress", function (e) { try { var data = JSON.parse(e.data); reviewStatusText.textContent = data.status; reviewFill.style.width = data.progress + "%"; reviewPercent.textContent = data.progress + "%"; } catch (err) {} }); es.addEventListener("review_done", function (e) { try { var data = JSON.parse(e.data); if (data.review_error) { showError("المراجعة: " + data.review_error); reviewBtn.disabled = false; reviewBtnText.textContent = "إعادة المراجعة"; reviewSpinner.classList.add("hidden"); } else { onReviewComplete(data); } loadPastJobs(); } catch (err) {} }); es.addEventListener("done", function (e) { es.close(); currentEventSource = null; }); es.addEventListener("keepalive", function () {}); es.onerror = function () { // Auto-reconnect is built into EventSource, but close after 60s of errors setTimeout(function () { if (es.readyState === EventSource.CLOSED) return; // If still failing, fall back to polling es.close(); currentEventSource = null; pollSearchFallback(jobId); }, 60000); }; return es; } function disconnectSSE() { if (currentEventSource) { currentEventSource.close(); currentEventSource = null; } } // ── Fallback polling (only if SSE fails) ───────────────────── function pollSearchFallback(jobId) { var interval = setInterval(async function () { try { var resp = await fetch("/flask/status/" + jobId); var data = await resp.json(); if (data.error) { clearInterval(interval); showError(data.error); return; } updateSearchUI(data); if (data.search_done) { clearInterval(interval); loadPastJobs(); if (data.write_done) { onWriteComplete(data); if (data.review_done) { onReviewComplete(data); } else if (data.review_in_progress) { pollReviewFallback(jobId); } } else if (data.write_progress > 0) { onSearchComplete(data, true); pollWriteFallback(jobId); } else { onSearchComplete(data); } } } catch (err) { clearInterval(interval); showError("خطأ في الاتصال: " + err.message); } }, 5000); } function pollWriteFallback(jobId) { var interval = setInterval(async function () { try { var resp = await fetch("/flask/status/" + jobId); var data = await resp.json(); if (data.error) { clearInterval(interval); showError(data.error); return; } writeStatusText.textContent = data.status; writeFill.style.width = data.write_progress + "%"; writePercent.textContent = data.write_progress + "%"; if (data.write_done) { clearInterval(interval); loadPastJobs(); if (data.write_error) { showError(data.write_error); writeBtn.disabled = false; writeBtnText.textContent = "إعادة المحاولة"; writeSpinner.classList.add("hidden"); } else { onWriteComplete(data); if (data.review_in_progress) { pollReviewFallback(jobId); } } } } catch (err) { clearInterval(interval); showError("خطأ في الاتصال: " + err.message); } }, 5000); } function pollReviewFallback(jobId) { var interval = setInterval(async function () { try { var resp = await fetch("/flask/status/" + jobId); var data = await resp.json(); if (data.error) { clearInterval(interval); showError(data.error); return; } reviewStatusText.textContent = data.status; reviewFill.style.width = data.review_progress + "%"; reviewPercent.textContent = data.review_progress + "%"; if (data.review_done) { clearInterval(interval); loadPastJobs(); if (data.review_error) { showError("المراجعة: " + data.review_error); reviewBtn.disabled = false; reviewBtnText.textContent = "إعادة المراجعة"; reviewSpinner.classList.add("hidden"); } else { onReviewComplete(data); } } } catch (err) { clearInterval(interval); showError("خطأ في الاتصال: " + err.message); } }, 5000); } function updateSearchUI(data) { statusText.textContent = data.status; progressFill.style.width = data.search_progress + "%"; progressPercent.textContent = data.search_progress + "%"; if (data.log && data.log.length > 0) { var logHtml = ""; data.log.forEach(function (entry) { logHtml += '
' + esc(entry) + "
"; }); logContent.innerHTML = logHtml; logContent.scrollTop = logContent.scrollHeight; } if (data.by_section && Object.keys(data.by_section).length > 0) { document.getElementById("section-breakdown").classList.remove("hidden"); renderBadges(data.by_section, sectionCards); } } function appendLog(entry) { if (!entry) return; var div = document.createElement("div"); div.className = "log-entry"; div.textContent = entry; logContent.appendChild(div); logContent.scrollTop = logContent.scrollHeight; } if (reviewBtn) { reviewBtn.addEventListener("click", async function () { if (!currentJobId) return; reviewBtn.disabled = true; reviewBtnText.textContent = "جاري المراجعة..."; reviewSpinner.classList.remove("hidden"); reviewProgressSection.classList.remove("hidden"); reviewFill.style.width = "0%"; reviewPercent.textContent = "0%"; reviewStatusText.textContent = "جاري بدء المراجعة..."; try { var resp = await fetch("/flask/review/" + currentJobId, { method: "POST", headers: { "X-CSRFToken": csrfToken } }); var data = await resp.json(); if (data.error) { showError(data.error); reviewBtn.disabled = false; reviewBtnText.textContent = "مراجعة لغوية"; reviewSpinner.classList.add("hidden"); reviewProgressSection.classList.add("hidden"); return; } loadPastJobs(); connectSSE(currentJobId); } catch (err) { showError("فشل بدء المراجعة: " + err.message); reviewBtn.disabled = false; reviewBtnText.textContent = "مراجعة لغوية"; reviewSpinner.classList.add("hidden"); reviewProgressSection.classList.add("hidden"); } }); } function onReviewComplete(data) { reviewBtn.disabled = false; reviewBtnText.textContent = "مراجعة لغوية"; reviewSpinner.classList.add("hidden"); reviewProgressSection.classList.add("hidden"); if (data.study_text) { setStudyContent(data.study_text); } if (data.study_file) { var actionsHtml = ""; actionsHtml += 'تحميل MD'; var dlPath = data.study_file.replace(/^\//, ''); actionsHtml += 'تحميل Word'; if (data.prompt_path) { actionsHtml += 'التعليمات'; } if (data.super_prompt_path) { actionsHtml += 'البرومبت الخارق'; } if (data.study_reviewed) { actionsHtml += 'تمت المراجعة'; } previewActions.innerHTML = actionsHtml; // Update full preview button with term name var viewFullBtn = previewActions.querySelector(".view-full-btn"); if (viewFullBtn && data.term) { viewFullBtn.setAttribute("data-term", data.term); } } if (data.audit) { renderAudit(data.audit); } else { auditSection.classList.add("hidden"); } } function onSearchComplete(data, isRestore) { submitBtn.disabled = false; btnText.classList.remove("hidden"); btnSpinner.classList.add("hidden"); filePaths.classList.remove("hidden"); // Helper: build a download URL from a relative path function mkDlUrl(relPath) { return "/flask/download/" + relPath.replace(/^\//, ''); } if (data.prompt_path) { rawPathRow.style.display = "flex"; rawPathEl.textContent = data.prompt_path; // Add download link next to the path var rawDlLink = rawPathRow.querySelector(".path-dl-link"); if (!rawDlLink) { rawDlLink = document.createElement("a"); rawDlLink.className = "dl-link-sm path-dl-link"; rawDlLink.textContent = "تحميل"; rawDlLink.target = "_blank"; rawPathRow.appendChild(rawDlLink); } rawDlLink.href = mkDlUrl(data.prompt_path); } if (data.super_prompt_path) { var spRow = document.getElementById("super-prompt-path-row"); spRow.style.display = "flex"; document.getElementById("super-prompt-path").textContent = data.super_prompt_path; var spLink = document.getElementById("super-prompt-dl-link"); if (spLink) spLink.href = mkDlUrl(data.super_prompt_path); } if (data.draft_path) { var draftRow = document.getElementById("draft-path-row"); draftRow.style.display = "flex"; document.getElementById("draft-path").textContent = data.draft_path; // Add download link next to the draft path var draftDlLink = draftRow.querySelector(".path-dl-link"); if (!draftDlLink) { draftDlLink = document.createElement("a"); draftDlLink.className = "dl-link-sm path-dl-link"; draftDlLink.textContent = "تحميل"; draftDlLink.target = "_blank"; draftRow.appendChild(draftDlLink); } draftDlLink.href = mkDlUrl(data.draft_path); } if (data.search_results_path) { var resultsRow = document.getElementById("results-path-row"); resultsRow.style.display = "flex"; document.getElementById("results-path").textContent = data.search_results_path; // Add download link next to the search results path var resultsDlLink = resultsRow.querySelector(".path-dl-link"); if (!resultsDlLink) { resultsDlLink = document.createElement("a"); resultsDlLink.className = "dl-link-sm path-dl-link"; resultsDlLink.textContent = "تحميل"; resultsDlLink.target = "_blank"; resultsRow.appendChild(resultsDlLink); } resultsDlLink.href = mkDlUrl(data.search_results_path); } if (data.abs_folder_path) { var absRow = document.getElementById("folder-abs-path-row"); if (absRow) { absRow.style.display = "flex"; document.getElementById("folder-abs-path").textContent = data.abs_folder_path; } } var bundleRow = document.getElementById("bundle-download-row"); var bundleLink = document.getElementById("download-zip-btn"); if (bundleRow && bundleLink) { var categoryVal = data.category || "tadeel"; var termVal = (data.term || "").replace(/ /g, "_"); if (termVal) { bundleRow.style.display = "block"; bundleLink.href = data.bundle_dl || ("/flask/download-bundle/" + categoryVal + "/" + encodeURIComponent(termVal)); } } searchPhase.classList.add("hidden"); // Hide search phase when complete writePhase.classList.remove("hidden"); writePhase.classList.add("fade-in"); writePhase.scrollIntoView({ behavior: "smooth", block: "start" }); // Highlight write button var wbtn = document.getElementById("write-btn"); if (wbtn) { wbtn.classList.add("write-btn-highlight"); setTimeout(function () { wbtn.classList.remove("write-btn-highlight"); }, 5000); } if (isRestore) { writeInfo.textContent = "تم جمع " + (data.total_results || 0) + " نتيجة. الدراسة متاحة للكتابة."; } else { var sectionLabel = { all: "بحث كامل", 1: "الدراسة اللغوية", 2: "الدراسة الاصطلاحية", 3: "استعمال القطان", 4: "الدراسة المقارنة", 5: "الخلاصة والنتائج", }[data.section] || "بحث كامل"; writeInfo.textContent = "تم جمع " + (data.total_results || 0) + " نتيجة (" + sectionLabel + ")، منها " + (data.qattan_evidence_count || 0) + " نتيجة بدليل نسبة للقطان. اضغط لبدء كتابة الدراسة الأكاديمية."; } if (manualWriteActions) { manualWriteActions.classList.remove("hidden"); } if (data.super_prompt_path) { if (downloadSuperPromptBtn) { downloadSuperPromptBtn.href = "/flask/download/" + data.super_prompt_path.replace(/^\//, ''); } if (copySuperPromptBtn) { copySuperPromptBtn.onclick = async function () { copySuperPromptBtn.disabled = true; var originalText = copySuperPromptBtn.textContent; copySuperPromptBtn.textContent = "جاري النسخ..."; try { var path = "/flask/download/" + data.super_prompt_path.replace(/^\//, ''); var r = await fetch(path); var text = await r.text(); await navigator.clipboard.writeText(text); copySuperPromptBtn.textContent = "تم النسخ!"; setTimeout(function () { copySuperPromptBtn.textContent = originalText; copySuperPromptBtn.disabled = false; }, 2000); } catch (err) { alert("فشل في نسخ النص: " + err.message); copySuperPromptBtn.textContent = originalText; copySuperPromptBtn.disabled = false; } }; } } if (data.section_details && Object.keys(data.section_details).length > 0) { renderBadges(data.section_details, sectionCards); } loadCompletedStudies(); } function onWriteComplete(data, isRestore) { writeBtn.disabled = false; writeBtnText.textContent = "✍️ اكتب الدراسة"; writeSpinner.classList.add("hidden"); writeProgressSection.classList.add("hidden"); writePhase.classList.add("hidden"); if (manualWriteActions) { manualWriteActions.classList.add("hidden"); } filePaths.classList.remove("hidden"); if (data.study_file) { studyPathRow.style.display = "flex"; studyPathEl.textContent = data.study_file; // Add or update download link next to the study path var studyDlLink = studyPathRow.querySelector(".path-dl-link"); if (!studyDlLink) { studyDlLink = document.createElement("a"); studyDlLink.className = "dl-link-sm path-dl-link"; studyDlLink.textContent = "تحميل"; studyDlLink.target = "_blank"; studyPathRow.appendChild(studyDlLink); } studyDlLink.href = "/flask/download/" + data.study_file.replace(/^\//, ''); } resultsSection.classList.remove("hidden"); resultsSection.classList.add("fade-in"); resultTitle.textContent = "تمت كتابة الدراسة"; var html = ""; html += '

اللفظ: ' + esc(data.term) + "

"; html += '

عدد النتائج: ' + (data.total_results || "—") + "

"; html += '

نتائج عليها دليل نسبة للقطان: ' + (data.qattan_evidence_count || 0) + "

"; html += '

نتائج تحتاج مراجعة: ' + (data.needs_review_count || 0) + "

"; summary.innerHTML = html; if (data.section_details && Object.keys(data.section_details).length > 0) { renderBadges(data.section_details, sectionDetail); } if (data.study_text) { studyPreview.classList.remove("hidden"); studyPreview.classList.add("fade-in"); reviewPhase.classList.remove("hidden"); reviewPhase.classList.add("fade-in"); var actionsHtml = ""; if (data.study_file) { actionsHtml += 'تحميل MD'; } if (data.study_file) { var dlPath = data.study_file.replace(/^\//, ''); actionsHtml += 'تحميل Word'; } if (data.prompt_path) { actionsHtml += 'التعليمات'; } if (data.super_prompt_path) { actionsHtml += 'البرومبت الخارق'; } if (data.study_reviewed) { actionsHtml += 'تمت المراجعة'; } previewActions.innerHTML = actionsHtml; // Update full preview button with term name var viewFullBtn = previewActions.querySelector(".view-full-btn"); if (viewFullBtn && data.term) { viewFullBtn.setAttribute("data-term", data.term); } setStudyContent(data.study_text); } if (data.audit) { renderAudit(data.audit); } else { auditSection.classList.add("hidden"); } loadCompletedStudies(); } function renderBadges(sections, container) { var labels = { 1: "لغوية", 2: "اصطلاحية", 3: "القطان", 4: "مقارنة", 5: "خلاصة", }; var html = ""; Object.keys(sections).forEach(function (key) { var s = sections[key]; html += '
' + (labels[key] || s.name || key) + ' ' + (s.count || "0") + "
"; }); container.innerHTML = html; } function renderAudit(audit) { if (!audit) { auditSection.classList.add("hidden"); return; } auditSection.classList.remove("hidden"); var severity = audit.severity || "PASS"; var badgeClass = severity.toLowerCase(); var badgeText = { "PASS": "ناجح (مطابق للمنهجية)", "WARN": "تنبيه (ملاحظات منهجية)", "FAIL": "راسب (مخالف للمنهجية)" }[severity] || severity; auditBadgeContainer.innerHTML = '' + badgeText + ''; var stats = audit.stats || {}; var statsHtml = ""; var statsConfig = [ { key: "words", label: "عدد الكلمات" }, { key: "total_citations", label: "إجمالي التوثيقات" }, { key: "citation_density", label: "كثافة التوثيق", format: function(v) { return Math.round(v * 100) + "%"; } }, { key: "paragraphs_without_citation", label: "فقرات بلا توثيق" }, { key: "english_words", label: "الكلمات الإنجليزية" }, { key: "found_sections", label: "الأقسام المكتملة", format: function(v) { return v + " / " + (stats.required_sections || 7); } } ]; statsConfig.forEach(function (cfg) { var val = stats[cfg.key]; if (val !== undefined) { var formattedVal = cfg.format ? cfg.format(val) : val; statsHtml += '
' + '' + esc(cfg.label) + '' + '' + esc(formattedVal) + '' + '
'; } }); auditStatsGrid.innerHTML = statsHtml; var issues = audit.issues || []; if (issues.length === 0) { auditIssuesList.innerHTML = '
' + '✓ لا توجد أي مخالفات منهجية أو لغوية.
'; } else { var issuesHtml = ""; issues.forEach(function (issue) { var issueClass = issue.severity === "error" ? "error" : "warning"; var icon = issue.severity === "error" ? "X" : "!"; var detailsHtml = ""; if (issue.details && Array.isArray(issue.details) && issue.details.length > 0) { detailsHtml = '
' + issue.details.map(function(d) { return esc(d); }).join('
') + '
'; } else if (issue.details && typeof issue.details === 'string') { detailsHtml = '
' + esc(issue.details) + '
'; } issuesHtml += '
' + '
' + '' + icon + ' [' + esc(issue.type) + '] ' + esc(issue.message) + detailsHtml + '
' + '
'; }); auditIssuesList.innerHTML = issuesHtml; } } function showErrorUI(msg) { submitBtn.disabled = false; btnText.classList.remove("hidden"); btnSpinner.classList.add("hidden"); if (writeBtn) { writeBtn.disabled = false; writeBtnText.textContent = "✍️ اكتب الدراسة"; writeSpinner.classList.add("hidden"); } if (reviewBtn) { reviewBtn.disabled = false; reviewBtnText.textContent = "مراجعة لغوية"; reviewSpinner.classList.add("hidden"); } resultsSection.classList.add("hidden"); errorSection.classList.remove("hidden"); errorSection.classList.add("fade-in"); errorText.textContent = typeof msg === "object" && msg.message ? msg.message : msg; } function showError(msg) { showErrorUI(msg); showToast(typeof msg === "string" ? msg : "خطأ", "error", 5000); } // C4: Simple inline Markdown → HTML renderer (no external lib needed) function renderMarkdown(md) { if (!md) return ""; var lines = md.split("\n"); var html = ""; var inPara = false; var inCodeBlock = false; var codeFence = null; var codeLines = []; function closePara() { if (inPara) { html += "

\n"; inPara = false; } } function flushCodeBlock() { if (inCodeBlock && codeLines.length > 0) { html += "
" + esc(codeLines.join("\n")) + "
\n"; } inCodeBlock = false; codeFence = null; codeLines = []; } function inlineFormat(text) { // Bold **text** text = text.replace(/\*\*([^*]+)\*\*/g, "$1"); // Italic *text* text = text.replace(/\*([^*]+)\*/g, "$1"); // Links [text](url) — sanitize URL to prevent javascript: attacks text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (m, label, url) { var safeUrl = url.replace(/"/flask/g, '"').replace(/javascript:/gi, ''); return '' + label + ''; }); // Plain https:// links → clickable text = text.replace(/(https:\/\/shamela\.ws\/\S+)/g, '🔗 رابط الشاملة'); // Citation-style: (يحتاج توثيقاً) text = text.replace(/\(يحتاج توثيقاً\)/g, '(يحتاج توثيقاً)'); return text; } lines.forEach(function(line) { var trimmed = line.trim(); // Fenced code block handling var fenceMatch = trimmed.match(/^(```|~~~)(.*)/); if (fenceMatch) { if (inCodeBlock) { // Closing fence — flush the code block if (codeFence === fenceMatch[1]) { flushCodeBlock(); return; } else { // Mismatched fence — treat as code line codeLines.push(line); return; } } else { // Opening fence — flush any pending paragraph, start code block closePara(); inCodeBlock = true; codeFence = fenceMatch[1]; // Skip the info string (e.g. ```python) on the opening line return; } } if (inCodeBlock) { codeLines.push(line); return; } if (!trimmed) { closePara(); return; } // Headings var hm = trimmed.match(/^(#{1,5})\s+(.+)/); if (hm) { closePara(); var lvl = Math.min(hm[1].length, 6); html += "" + inlineFormat(esc(hm[2])) + "\n"; return; } // Horizontal rule if (/^---+$/.test(trimmed)) { closePara(); html += "
\n"; return; } // List item if (/^[\-\*]\s+/.test(trimmed)) { closePara(); html += "
  • " + inlineFormat(esc(trimmed.replace(/^[\-\*]\s+/, ""))) + "
  • \n"; return; } // Normal paragraph line if (!inPara) { html += "

    "; inPara = true; } else { html += " "; } html += inlineFormat(esc(trimmed)); }); flushCodeBlock(); closePara(); return html; } function setStudyContent(text) { if (!text) return; currentStudyFullText = text.trim(); currentStudyRenderedLength = LOAD_MORE_CHUNK; var truncated = currentStudyFullText.substring(0, LOAD_MORE_CHUNK); var html = renderMarkdown(truncated); if (currentStudyFullText.length > LOAD_MORE_CHUNK) { var remaining = currentStudyFullText.length - LOAD_MORE_CHUNK; html += '

    ' + '
    '; } studyContent.innerHTML = html; } function loadMoreStudy() { if (!currentStudyFullText) return; var nextEnd = Math.min(currentStudyRenderedLength + LOAD_MORE_CHUNK, currentStudyFullText.length); var newChunk = currentStudyFullText.substring(currentStudyRenderedLength, nextEnd); var container = document.getElementById('load-more-container'); if (container) container.remove(); studyContent.innerHTML += renderMarkdown(newChunk); currentStudyRenderedLength = nextEnd; if (currentStudyRenderedLength < currentStudyFullText.length) { var remaining = currentStudyFullText.length - currentStudyRenderedLength; studyContent.innerHTML += '
    ' + '
    '; } } function esc(str) { if (!str) return ""; return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/flask/g, """); } // ── Keyboard Shortcuts ──────────────────────────────────────── document.addEventListener("keydown", function (e) { // Ctrl+Enter or Cmd+Enter to submit search if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); if (!submitBtn.disabled) { form.dispatchEvent(new Event("submit")); } } // Escape to close error section if (e.key === "Escape") { errorSection.classList.add("hidden"); } }); // ── Toast Notifications ────────────────────────────────────── function showToast(message, type, duration) { type = type || "info"; duration = duration || 4000; var toast = document.createElement("div"); toast.className = "toast toast-" + type; toast.textContent = message; toast.style.cssText = "position:fixed;top:1.5rem;left:50%;transform:translateX(-50%);" + "padding:0.8rem 1.5rem;border-radius:12px;font-weight:600;font-size:0.9rem;" + "z-index:9999;box-shadow:0 8px 24px rgba(0,0,0,0.15);transition:all 0.3s ease;" + "opacity:0;translate:0 -10px;font-family:var(--font-arabic);"; if (type === "error") { toast.style.background = "#dc2626"; toast.style.color = "#fff"; } else if (type === "success") { toast.style.background = "#059669"; toast.style.color = "#fff"; } else { toast.style.background = "#2563eb"; toast.style.color = "#fff"; } document.body.appendChild(toast); requestAnimationFrame(function () { toast.style.opacity = "1"; toast.style.translate = "0 0"; }); setTimeout(function () { toast.style.opacity = "0"; toast.style.translate = "0 -10px"; setTimeout(function () { toast.remove(); }, 300); }, duration); } // ── New Session: disconnect SSE ────────────────────────────── if (newSessionBtn) { newSessionBtn.addEventListener("click", function () { disconnectSSE(); }); } // ── Dark mode auto-detect ──────────────────────────────────── if (!localStorage.getItem("theme")) { if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { document.body.classList.add("dark-theme"); if (themeToggle) { themeToggle.querySelector(".theme-icon").textContent = "☀️"; themeToggle.querySelector(".theme-text").textContent = "المظهر المضيء"; } } } // ── Full Study Preview Modal ───────────────────────────────── var modal = document.getElementById("study-modal"); var modalTitle = document.getElementById("modal-title"); var modalContent = document.getElementById("modal-study-content"); var modalClose = document.getElementById("modal-close"); var modalCopy = document.getElementById("modal-copy"); var modalExport = document.getElementById("modal-export"); function openStudyModal(studyText, termName) { if (!modal) return; modalTitle.textContent = "معاينة الدراسة: " + (termName || ""); modalContent.innerHTML = renderMarkdown(studyText || ""); modal.classList.remove("hidden"); document.body.style.overflow = "hidden"; } function closeStudyModal() { if (!modal) return; modal.classList.add("hidden"); document.body.style.overflow = ""; } if (modalClose) { modalClose.addEventListener("click", closeStudyModal); } if (modal) { modal.addEventListener("click", function (e) { if (e.target === modal) closeStudyModal(); }); } document.addEventListener("keydown", function (e) { if (e.key === "Escape" && modal && !modal.classList.contains("hidden")) { closeStudyModal(); } }); // Copy full study text from modal if (modalCopy) { modalCopy.addEventListener("click", function () { var text = modalContent.innerText || modalContent.textContent; navigator.clipboard.writeText(text).then(function () { showToast("تم نسخ النص", "success"); }); }); } // Export from modal (calls the existing export function) if (modalExport) { modalExport.addEventListener("click", function () { closeStudyModal(); var exportBtn = document.getElementById("export-btn"); if (exportBtn && !exportBtn.disabled) { exportBtn.click(); } }); } // ── Full Study Preview Modal ───────────────────────────────── var modal = document.getElementById("study-modal"); var modalTitle = document.getElementById("modal-title"); var modalContent = document.getElementById("modal-study-content"); var modalClose = document.getElementById("modal-close"); var modalCopy = document.getElementById("modal-copy"); var modalExport = document.getElementById("modal-export"); function openStudyModal(studyText, termName) { if (!modal) return; modalTitle.textContent = "معاينة الدراسة: " + (termName || ""); modalContent.innerHTML = renderMarkdown(studyText || ""); modal.classList.remove("hidden"); document.body.style.overflow = "hidden"; } function closeStudyModal() { if (!modal) return; modal.classList.add("hidden"); document.body.style.overflow = ""; } if (modalClose) { modalClose.addEventListener("click", closeStudyModal); } if (modal) { modal.addEventListener("click", function (e) { if (e.target === modal) closeStudyModal(); }); } document.addEventListener("keydown", function (e) { if (e.key === "Escape" && modal && !modal.classList.contains("hidden")) { closeStudyModal(); } }); // Copy full study text from modal if (modalCopy) { modalCopy.addEventListener("click", function () { var text = modalContent.innerText || modalContent.textContent; navigator.clipboard.writeText(text).then(function () { showToast("تم نسخ النص", "success"); }); }); } // Export from modal (calls the existing export function) if (modalExport) { modalExport.addEventListener("click", function () { closeStudyModal(); var exportBtn = document.getElementById("export-btn"); if (exportBtn && !exportBtn.disabled) { exportBtn.click(); } }); } // ── Event delegation for dynamic content ────────────────────── // Use event delegation for dynamically created buttons (e.g. after write completes) // so click handlers survive innerHTML replacements. document.addEventListener("click", function (e) { var target = e.target.closest(".view-full-btn"); if (!target) return; var term = target.getAttribute("data-term"); if (currentJobId) { fetch("/flask/study/" + currentJobId) .then(function (r) { return r.json(); }) .then(function (data) { if (data.study_text) { openStudyModal(data.study_text, term || data.term); return; } }) .catch(function () {}); } openStudyModal(currentStudyFullText || studyContent.innerText, term); }); loadSavedJob(); loadPastJobs(); loadCompletedStudies(); })();