Spaces:
Running on Zero
Running on Zero
Gradio launch (ZeroGPU detection) + Flask mounted at /flask; frontend paths prefixed
f65a0e1 verified | (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 = | |
| '<div class="skeleton skeleton-card"></div>' + | |
| '<div class="skeleton skeleton-card"></div>' + | |
| '<div class="skeleton skeleton-card"></div>'; | |
| try { | |
| var resp = await fetch("/flask/jobs"); | |
| var data = await resp.json(); | |
| if (!data || data.length === 0) { | |
| pastJobsList.innerHTML = '<p class="no-jobs">لا توجد جلسات نشطة حالياً.</p>'; | |
| 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 += '<div class="' + jobClass + '" data-id="' + job.job_id + '">' + | |
| '<span class="job-term">' + esc(job.term) + ' (' + (job.category === "tadeel" ? "تعديل" : "جرح") + ')</span>' + | |
| '<span class="job-status">' + esc(statusLabel) + '</span>' + | |
| '<span class="job-meta">' + esc(dateStr) + '</span>' + | |
| '<button class="job-delete-btn" data-id="' + job.job_id + '" title="حذف الجلسة">✕</button>' + | |
| '</div>'; | |
| }); | |
| 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 = | |
| '<div class="skeleton skeleton-card"></div>' + | |
| '<div class="skeleton skeleton-card"></div>'; | |
| try { | |
| var resp = await fetch("/flask/completed"); | |
| var data = await resp.json(); | |
| if (!data || data.length === 0) { | |
| completedList.innerHTML = '<p class="no-jobs">لا توجد دراسات مكتملة بعد.</p>'; | |
| 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 += '<a href="' + s.study_dl + '" class="dl-link-sm" target="_blank" title="تحميل study.md" onclick="event.stopPropagation()">MD</a>'; | |
| } | |
| if (s.docx_dl) { | |
| dlBtns += '<a href="' + s.docx_dl + '" class="dl-link-sm" target="_blank" title="تحميل Word" onclick="event.stopPropagation()">Word</a>'; | |
| } | |
| 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 += '<div class="completed-item"' + dataAttrs + clickable + '>' + | |
| '<div class="completed-header">' + | |
| '<span class="completed-term">' + esc(s.term) + '</span>' + | |
| '<span class="completed-cat">' + esc(s.category) + '</span>' + | |
| '</div>' + | |
| '<span class="completed-status ' + statusClass + '">' + esc(s.status) + '</span>' + | |
| (meta ? '<span class="completed-meta">' + esc(meta) + '</span>' : '') + | |
| (dlBtns ? '<div class="completed-actions">' + dlBtns + '</div>' : '') + | |
| '</div>'; | |
| }); | |
| 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 = '<p class="no-jobs">فشل تحميل الدراسات.</p>'; | |
| 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 += '<div class="log-entry">' + esc(entry) + "</div>"; | |
| }); | |
| 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 += | |
| '<a href="/flask/download/' + data.study_file.replace(/^\//, '') + | |
| '" class="dl-link">تحميل MD</a>'; | |
| var dlPath = data.study_file.replace(/^\//, ''); | |
| actionsHtml += | |
| '<a href="/flask/download/docx/' + dlPath + | |
| '" class="dl-link">تحميل Word</a>'; | |
| if (data.prompt_path) { | |
| actionsHtml += | |
| '<a href="/flask/download/' + data.prompt_path.replace(/^\//, '') + | |
| '" class="dl-link">التعليمات</a>'; | |
| } | |
| if (data.super_prompt_path) { | |
| actionsHtml += | |
| '<a href="/flask/download/' + data.super_prompt_path.replace(/^\//, '') + | |
| '" class="dl-link">البرومبت الخارق</a>'; | |
| } | |
| if (data.study_reviewed) { | |
| actionsHtml += '<span class="reviewed-badge">تمت المراجعة</span>'; | |
| } | |
| 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 += '<p><strong>اللفظ:</strong> ' + esc(data.term) + "</p>"; | |
| html += '<p><strong>عدد النتائج:</strong> ' + (data.total_results || "—") + "</p>"; | |
| html += '<p><strong>نتائج عليها دليل نسبة للقطان:</strong> ' + (data.qattan_evidence_count || 0) + "</p>"; | |
| html += '<p><strong>نتائج تحتاج مراجعة:</strong> ' + (data.needs_review_count || 0) + "</p>"; | |
| 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 += | |
| '<a href="/flask/download/' + data.study_file.replace(/^\//, '') + | |
| '" class="dl-link">تحميل MD</a>'; | |
| } | |
| if (data.study_file) { | |
| var dlPath = data.study_file.replace(/^\//, ''); | |
| actionsHtml += | |
| '<a href="/flask/download/docx/' + dlPath + | |
| '" class="dl-link">تحميل Word</a>'; | |
| } | |
| if (data.prompt_path) { | |
| actionsHtml += | |
| '<a href="/flask/download/' + data.prompt_path.replace(/^\//, '') + | |
| '" class="dl-link">التعليمات</a>'; | |
| } | |
| if (data.super_prompt_path) { | |
| actionsHtml += | |
| '<a href="/flask/download/' + data.super_prompt_path.replace(/^\//, '') + | |
| '" class="dl-link">البرومبت الخارق</a>'; | |
| } | |
| if (data.study_reviewed) { | |
| actionsHtml += '<span class="reviewed-badge">تمت المراجعة</span>'; | |
| } | |
| 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 += | |
| '<div class="sec-badge">' + | |
| (labels[key] || s.name || key) + | |
| ' <span class="sec-count">' + | |
| (s.count || "0") + "</span></div>"; | |
| }); | |
| 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 = '<span class="audit-badge ' + badgeClass + '">' + badgeText + '</span>'; | |
| 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 += '<div class="audit-stat-item">' + | |
| '<span>' + esc(cfg.label) + '</span>' + | |
| '<span>' + esc(formattedVal) + '</span>' + | |
| '</div>'; | |
| } | |
| }); | |
| auditStatsGrid.innerHTML = statsHtml; | |
| var issues = audit.issues || []; | |
| if (issues.length === 0) { | |
| auditIssuesList.innerHTML = '<div class="audit-issue-item pass" style="color: var(--success); font-weight: bold; padding: 0.5rem 0;">' + | |
| '✓ لا توجد أي مخالفات منهجية أو لغوية.</div>'; | |
| } 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 = '<div style="margin-top: 4px; padding-right: 15px; font-size: 0.7rem; opacity: 0.85; word-break: break-all;">' + | |
| issue.details.map(function(d) { return esc(d); }).join('<br>') + | |
| '</div>'; | |
| } else if (issue.details && typeof issue.details === 'string') { | |
| detailsHtml = '<div style="margin-top: 4px; padding-right: 15px; font-size: 0.7rem; opacity: 0.85; word-break: break-all;">' + | |
| esc(issue.details) + | |
| '</div>'; | |
| } | |
| issuesHtml += '<div class="audit-issue-item ' + issueClass + '">' + | |
| '<div>' + | |
| '<strong>' + icon + ' [' + esc(issue.type) + ']</strong> ' + esc(issue.message) + | |
| detailsHtml + | |
| '</div>' + | |
| '</div>'; | |
| }); | |
| 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 += "</p>\n"; inPara = false; } | |
| } | |
| function flushCodeBlock() { | |
| if (inCodeBlock && codeLines.length > 0) { | |
| html += "<pre><code>" + esc(codeLines.join("\n")) + "</code></pre>\n"; | |
| } | |
| inCodeBlock = false; | |
| codeFence = null; | |
| codeLines = []; | |
| } | |
| function inlineFormat(text) { | |
| // Bold **text** | |
| text = text.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"); | |
| // Italic *text* | |
| text = text.replace(/\*([^*]+)\*/g, "<em>$1</em>"); | |
| // 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 '<a href="' + safeUrl + '" target="_blank" rel="noopener">' + label + '</a>'; | |
| }); | |
| // Plain https:// links → clickable | |
| text = text.replace(/(https:\/\/shamela\.ws\/\S+)/g, | |
| '<a href="$1" target="_blank" rel="noopener" class="shamela-link">🔗 رابط الشاملة</a>'); | |
| // Citation-style: (يحتاج توثيقاً) | |
| text = text.replace(/\(يحتاج توثيقاً\)/g, | |
| '<span class="needs-citation">(يحتاج توثيقاً)</span>'); | |
| 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 += "<h" + lvl + ">" + inlineFormat(esc(hm[2])) + "</h" + lvl + ">\n"; | |
| return; | |
| } | |
| // Horizontal rule | |
| if (/^---+$/.test(trimmed)) { | |
| closePara(); | |
| html += "<hr>\n"; | |
| return; | |
| } | |
| // List item | |
| if (/^[\-\*]\s+/.test(trimmed)) { | |
| closePara(); | |
| html += "<li>" + inlineFormat(esc(trimmed.replace(/^[\-\*]\s+/, ""))) + "</li>\n"; | |
| return; | |
| } | |
| // Normal paragraph line | |
| if (!inPara) { html += "<p dir='rtl'>"; 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 += '<div id="load-more-container" style="text-align:center; margin:1rem 0;">' + | |
| '<button id="load-more-btn" class="action-btn secondary load-more-btn" onclick="loadMoreStudy()">' + | |
| 'عرض المزيد (' + remaining + ' حرف متبقي)</button></div>'; | |
| } | |
| 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 += '<div id="load-more-container" style="text-align:center; margin:1rem 0;">' + | |
| '<button id="load-more-btn" class="action-btn secondary load-more-btn" onclick="loadMoreStudy()">' + | |
| 'عرض المزيد (' + remaining + ' حرف متبقي)</button></div>'; | |
| } | |
| } | |
| function esc(str) { | |
| if (!str) return ""; | |
| return str | |
| .replace(/&/g, "&") | |
| .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(); | |
| })(); | |