alfaz-browser / static /script.js
abedelbahnasy55's picture
Gradio launch (ZeroGPU detection) + Flask mounted at /flask; frontend paths prefixed
f65a0e1 verified
Raw
History Blame Contribute Delete
63.7 kB
(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, '&quot;').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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/flask/g, "&quot;");
}
// ── 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();
})();